diff --git a/AKModel/forms.py b/AKModel/forms.py
index 9ec3d24ec5f73148be7f135b8aa9f97965b3a4cc..eb60a04071ba1267681176ccd32d7e79e6437492 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -4,13 +4,17 @@ Central and admin forms
 
 import csv
 import io
+import json
 
 from django import forms
+from django.core.exceptions import ValidationError
 from django.forms.utils import ErrorList
 from django.utils.translation import gettext_lazy as _
+from jsonschema.exceptions import best_match
 
 from AKModel.availability.forms import AvailabilitiesFormMixin
 from AKModel.models import Event, AKCategory, AKRequirement, Room, AKType
+from AKModel.utils import construct_schema_validator
 
 
 class DateTimeInput(forms.DateInput):
@@ -286,8 +290,66 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
 class JSONScheduleImportForm(AdminIntermediateForm):
     """Form to import an AK schedule from a json file."""
     json_data = forms.CharField(
-        required=True,
+        required=False,
         widget=forms.Textarea,
         label=_("JSON data"),
         help_text=_("JSON data from the scheduling solver"),
     )
+
+    json_file = forms.FileField(
+        required=False,
+        label=_("File with JSON data"),
+        help_text=_("File with JSON data from the scheduling solver"),
+    )
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.json_schema_validator = construct_schema_validator(
+            schema="solver-output.schema.json"
+        )
+
+    def _check_json_data(self, data: str):
+        try:
+            schedule = json.loads(data)
+        except json.JSONDecodeError as ex:
+            raise ValidationError(_("Cannot decode as JSON"), "invalid") from ex
+
+        error = best_match(self.json_schema_validator.iter_errors(schedule))
+        if error:
+            raise ValidationError(
+                _("Invalid JSON format: %(msg)s at %(error_path)s"),
+                "invalid",
+                params={
+                    "msg": error.message,
+                    "error_path": error.json_path
+                }
+            ) from error
+
+        return schedule
+
+    def clean(self):
+        cleaned_data = super().clean()
+        if cleaned_data.get("json_file") and cleaned_data.get("json_data"):
+            err = ValidationError(
+                _("Please enter data as a file OR via text, not both."), "invalid"
+            )
+            self.add_error("json_data", err)
+            self.add_error("json_file", err)
+        elif not (cleaned_data.get("json_file") or cleaned_data.get("json_data")):
+            err = ValidationError(
+                _("No data entered. Please enter data as a file or via text."), "invalid"
+            )
+            self.add_error("json_data", err)
+            self.add_error("json_file", err)
+        else:
+            source_field = "json_data"
+            data = cleaned_data.get(source_field)
+            if not data:
+                source_field = "json_file"
+                with cleaned_data.get(source_field).open() as ff:
+                    data = ff.read()
+            try:
+                cleaned_data["data"] = self._check_json_data(data)
+            except ValidationError as ex:
+                self.add_error(source_field, ex)
+        return cleaned_data
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 00409411289c42f855f3f8a048a4175d87424800..1c556efdaa0f51d695aad19a22b451a5b041d8d6 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -180,60 +180,60 @@ msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/forms.py:78
+#: AKModel/forms.py:80
 msgid "Copy ak requirements and ak categories of existing event"
 msgstr "AK-Anforderungen und AK-Kategorien eines existierenden Events kopieren"
 
-#: AKModel/forms.py:79
+#: AKModel/forms.py:81
 msgid "You can choose what to copy in the next step"
 msgstr ""
 "Im nächsten Schritt kann ausgewählt werden, was genau kopiert werden soll"
 
-#: AKModel/forms.py:93
+#: AKModel/forms.py:95
 msgid "Copy ak categories"
 msgstr "AK-Kategorien kopieren"
 
-#: AKModel/forms.py:100
+#: AKModel/forms.py:102
 msgid "Copy ak requirements"
 msgstr "AK-Anforderungen kopieren"
 
-#: AKModel/forms.py:107
+#: AKModel/forms.py:109
 msgid "Copy types"
 msgstr "Typen kopieren"
 
-#: AKModel/forms.py:133
+#: AKModel/forms.py:135
 msgid "Copy dashboard buttons"
 msgstr "Dashboard-Buttons kopieren"
 
-#: AKModel/forms.py:174
+#: AKModel/forms.py:176
 msgid "# next AKs"
 msgstr "# nächste AKs"
 
-#: AKModel/forms.py:175
+#: AKModel/forms.py:177
 msgid "How many next AKs should be shown on a slide?"
 msgstr "Wie viele nächste AKs sollen auf einer Folie angezeigt werden?"
 
-#: AKModel/forms.py:178
+#: AKModel/forms.py:180
 msgid "Presentation only?"
 msgstr "Nur Vorstellung?"
 
-#: AKModel/forms.py:180 AKModel/forms.py:187
+#: AKModel/forms.py:182 AKModel/forms.py:189
 msgid "Yes"
 msgstr "Ja"
 
-#: AKModel/forms.py:180 AKModel/forms.py:187
+#: AKModel/forms.py:182 AKModel/forms.py:189
 msgid "No"
 msgstr "Nein"
 
-#: AKModel/forms.py:182
+#: AKModel/forms.py:184
 msgid "Restrict AKs to those that asked for chance to be presented?"
 msgstr "AKs auf solche, die um eine Vorstellung gebeten haben, einschränken?"
 
-#: AKModel/forms.py:185
+#: AKModel/forms.py:187
 msgid "Space for notes in wishes?"
 msgstr "Platz für Notizen bei den Wünschen?"
 
-#: AKModel/forms.py:189
+#: AKModel/forms.py:191
 msgid ""
 "Create symbols indicating space to note down owners and timeslots for "
 "wishes, e.g., to be filled out on a touch screen while presenting?"
@@ -246,7 +246,7 @@ msgstr ""
 msgid "Default Slots"
 msgstr "Standardslots"
 
-#: AKModel/forms.py:200
+#: AKModel/forms.py:202
 msgid ""
 "Click and drag to add default slots, double-click to delete. Or use the "
 "start and end inputs to add entries to the calendar view."
@@ -255,11 +255,11 @@ msgstr ""
 "Einträge zu löschen. Oder Start- und End-Eingabe verwenden, um der "
 "Kalenderansicht neue Einträge hinzuzufügen."
 
-#: AKModel/forms.py:216
+#: AKModel/forms.py:218
 msgid "New rooms"
 msgstr "Neue Räume"
 
-#: AKModel/forms.py:217
+#: AKModel/forms.py:219
 msgid ""
 "Enter room details in CSV format. Required colum is \"name\", optional "
 "colums are \"location\", \"capacity\", and \"url\" for online/hybrid rooms. "
@@ -269,26 +269,51 @@ msgstr ""
 "Spalten sind \"location\", \"capacity\", und \"url\" for Online-/"
 "HybridräumeTrennzeichen: Semikolon"
 
-#: AKModel/forms.py:223
+#: AKModel/forms.py:225
 msgid "Default availabilities?"
 msgstr "Standardverfügbarkeiten?"
 
-#: AKModel/forms.py:224
+#: AKModel/forms.py:226
 msgid "Create default availabilities for all rooms?"
 msgstr "Standardverfügbarkeiten für alle Räume anlegen?"
 
-#: AKModel/forms.py:240
+#: AKModel/forms.py:242
 msgid "CSV must contain a name column"
 msgstr "CSV muss eine name-Spalte enthalten"
 
-#: AKModel/forms.py:291
+#: AKModel/forms.py:293
 msgid "JSON data"
 msgstr "JSON-Daten"
 
-#: AKModel/forms.py:292
+#: AKModel/forms.py:294
 msgid "JSON data from the scheduling solver"
 msgstr "JSON-Daten, die der scheduling-solver produziert hat"
 
+#: AKModel/forms.py:299
+msgid "File with JSON data"
+msgstr "Datei mit JSON-Daten"
+
+#: AKModel/forms.py:300
+msgid "File with JSON data from the scheduling solver"
+msgstr "Datei mit JSON-Daten, die der scheduling-solver produziert hat"
+
+#: AKModel/forms.py:307
+msgid "Cannot decode as JSON"
+msgstr "Dekodierung als JSON fehlgeschlagen"
+
+#: AKModel/forms.py:311
+#, python-format
+msgid "Invalid JSON format: %(msg)s at %(error_path)s"
+msgstr "Ungültige JSON-Eingabe: %(msg)s bei %(error_path)s"
+
+#: AKModel/forms.py:321
+msgid "Please enter data as a file OR via text, not both."
+msgstr "Gib die Daten bitte als Datei oder als Text ein, nicht beides."
+
+#: AKModel/forms.py:327
+msgid "No data entered. Please enter data as a file or via text."
+msgstr "Keine Daten eingegeben. Gib die Daten bitte als Datei oder als Text ein."
+
 #: AKModel/metaviews/admin.py:156 AKModel/models.py:141
 msgid "Start"
 msgstr "Start"
@@ -478,6 +503,14 @@ msgstr ""
 msgid "Events"
 msgstr "Events"
 
+#: AKModel/models.py:427
+msgid "Cannot parse malformed JSON input."
+msgstr "Kann fehlerhafte JSON-Eingabe nicht verarbeiten"
+
+#: AKModel/models.py:430
+msgid "Data has changed since the export. Reexport and run the solver again."
+msgstr "Seit dem Export wurden die Daten verändert. Wiederhole den Export und führe den Solver erneut aus."
+
 #: AKModel/models.py:457
 #, python-brace-format
 msgid "AK {ak_name} is not assigned any timeslot by the solver"
@@ -1056,6 +1089,7 @@ msgid "Logout"
 msgstr "Ausloggen"
 
 #: AKModel/templates/admin/AKModel/action_intermediate.html:23
+#: AKModel/templates/admin/AKModel/import_json.html:23
 msgid "Confirm"
 msgstr "Bestätigen"
 
@@ -1063,6 +1097,7 @@ msgstr "Bestätigen"
 #: AKModel/templates/admin/AKModel/event_wizard/import.html:27
 #: AKModel/templates/admin/AKModel/event_wizard/settings.html:32
 #: AKModel/templates/admin/AKModel/event_wizard/start.html:28
+#: AKModel/templates/admin/AKModel/import_json.html:27
 #: AKModel/templates/admin/AKModel/room_create.html:30
 msgid "Cancel"
 msgstr "Abbrechen"
@@ -1375,16 +1410,16 @@ msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)"
 msgstr ""
 "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht"
 
-#: AKModel/views/manage.py:257
+#: AKModel/views/manage.py:258
 msgid "AK Schedule JSON Import"
 msgstr "AK-Plan JSON-Import"
 
-#: AKModel/views/manage.py:265
+#: AKModel/views/manage.py:274
 #, python-brace-format
 msgid "Successfully imported {n} slot(s)"
 msgstr "Erfolgreich {n} Slot(s) importiert"
 
-#: AKModel/views/manage.py:271
+#: AKModel/views/manage.py:280
 msgid "Importing an AK schedule failed! Reason: "
 msgstr "AK-Plan importieren fehlgeschlagen! Grund: "
 
@@ -1463,6 +1498,10 @@ msgstr "Zu Anforderungen gehörige AKs anzeigen"
 msgid "Event Status"
 msgstr "Eventstatus"
 
+#, python-format
+#~ msgid "Invalid JSON format: field '%(field)s' is missing"
+#~ msgstr "Ungültige JSON-Eingabe: das Feld '%(field)s' fehlt"
+
 #~ msgid "Opening time for expression of interest."
 #~ msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
diff --git a/AKModel/models.py b/AKModel/models.py
index 15da9170ff93ecb97144710203c2dfba8cf506a5..5e742434027b6c14c732f779d0f730175ee6d446 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -423,7 +423,9 @@ class Event(models.Model):
             yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour)
 
     @transaction.atomic
-    def schedule_from_json(self, schedule: str, *, check_for_data_inconsistency: bool = True) -> int:
+    def schedule_from_json(
+        self, schedule: str | dict[str, Any], *, check_for_data_inconsistency: bool = True
+    ) -> int:
         """Load AK schedule from a json string.
 
         :param schedule: A string that can be decoded to json, describing
@@ -431,11 +433,15 @@ class Event(models.Model):
             following the output specification of the KoMa conference optimizer, cf.
             https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
         """
-        schedule = json.loads(schedule)
+        if isinstance(schedule, str):
+            schedule = json.loads(schedule)
         export_dict = self.as_json_dict()
 
+        if "input" not in schedule or "scheduled_aks" not in schedule:
+            raise ValueError(_("Cannot parse malformed JSON input."))
+
         if check_for_data_inconsistency and schedule["input"] != export_dict:
-            raise ValueError("Data has changed since the export. Reexport and run the solver again.")
+            raise ValueError(_("Data has changed since the export. Reexport and run the solver again."))
 
         slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
 
diff --git a/AKModel/templates/admin/AKModel/import_json.html b/AKModel/templates/admin/AKModel/import_json.html
new file mode 100644
index 0000000000000000000000000000000000000000..737331e89f5614b9ef22fd68cea2f669fc79f91b
--- /dev/null
+++ b/AKModel/templates/admin/AKModel/import_json.html
@@ -0,0 +1,30 @@
+{% extends "admin/base_site.html" %}
+{% load tags_AKModel %}
+
+{% load i18n %}
+{% load django_bootstrap5 %}
+{% load fontawesome_6 %}
+
+
+{% block title %}{{event}}: {{ title }}{% endblock %}
+
+{% block content %}
+    {% block action_preview %}
+        <p>
+            {{ preview|linebreaksbr }}
+        </p>
+    {% endblock %}
+
+    <form enctype="multipart/form-data" method="post">{% csrf_token %}
+        {% bootstrap_form form %}
+
+        <div class="float-end">
+            <button type="submit" class="save btn btn-success" value="Submit">
+                {% fa6_icon "check" 'fas' %} {% trans "Confirm" %}
+            </button>
+        </div>
+        <a href="javascript:history.back()" class="btn btn-info">
+            {% fa6_icon "times" 'fas' %} {% trans "Cancel" %}
+        </a>
+    </form>
+{% endblock %}
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index 3acb05fd29a6e91cd17f45e9ed43d889a67da22c..1a7f1930b079edc4061ac5bc0a145466640bd574 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -253,12 +253,13 @@ class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView):
     """
     View: Import an AK schedule from a json file that can be pasted into this view.
     """
+    template_name = "admin/AKModel/import_json.html"
     form_class = JSONScheduleImportForm
     title = _("AK Schedule JSON Import")
 
     def form_valid(self, form):
         try:
-            number_of_slots_changed = self.event.schedule_from_json(form.data["json_data"])
+            number_of_slots_changed = self.event.schedule_from_json(form.cleaned_data["data"])
             messages.add_message(
                 self.request,
                 messages.SUCCESS,