diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index e8aac8fed36ba4f6917c3e22e87ae2ebec739775..5ec117497b2afd6bdb54cbc1507485303a2df451 100644
--- a/AKModel/tests/test_json_export.py
+++ b/AKModel/tests/test_json_export.py
@@ -1,6 +1,5 @@
 import json
 import math
-
 from collections import defaultdict
 from collections.abc import Iterable
 from datetime import datetime, timedelta
@@ -10,17 +9,11 @@ from bs4 import BeautifulSoup
 from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.urls import reverse
+from jsonschema.exceptions import best_match
 
 from AKModel.availability.models import Availability
-from AKModel.models import (
-    Event,
-    AKOwner,
-    AKCategory,
-    AK,
-    Room,
-    AKSlot,
-    DefaultSlot,
-)
+from AKModel.models import AK, AKCategory, AKOwner, AKSlot, DefaultSlot, Event, Room
+from AKModel.utils import construct_schema_validator
 
 
 class JSONExportTest(TestCase):
@@ -44,6 +37,10 @@ class JSONExportTest(TestCase):
             is_active=True,
         )
 
+        cls.json_export_validator = construct_schema_validator(
+            "solver-input.schema.json"
+        )
+
     def setUp(self):
         self.client.force_login(self.admin_user)
         self.export_dict = {}
@@ -119,204 +116,43 @@ class JSONExportTest(TestCase):
         if contained_type in {str, int}:
             self._check_uniqueness(lst, name, key=None)
 
-    def test_ak_conformity_to_spec(self):
-        """Test if AK JSON structure and types conform to standard."""
+    def test_conformity_to_schema(self):
+        """Test if JSON structure and types conform to schema."""
         for event in Event.objects.all():
             with self.subTest(event=event):
                 self.set_up_event(event=event)
 
-                self._check_uniqueness(self.export_dict["aks"], "AK")
-                for ak in self.export_dict["aks"]:
-                    item = f"AK {ak['id']}"
-                    self.assertEqual(
-                        ak.keys(),
-                        {
-                            "id",
-                            "duration",
-                            "properties",
-                            "room_constraints",
-                            "time_constraints",
-                            "info",
-                        },
-                        f"{item} keys not as expected",
-                    )
-                    self.assertEqual(
-                        ak["info"].keys(),
-                        {
-                            "name",
-                            "head",
-                            "description",
-                            "reso",
-                            "duration_in_hours",
-                            "django_ak_id",
-                            "types",
-                        },
-                        f"{item} info keys not as expected",
-                    )
-                    self.assertEqual(
-                        ak["properties"].keys(),
-                        {"conflicts", "dependencies"},
-                        f"{item} properties keys not as expected",
-                    )
-
-                    self._check_type(ak["id"], int, "id", item=item)
-                    self._check_type(ak["duration"], int, "duration", item=item)
-                    self._check_type(ak["info"]["name"], str, "info/name", item=item)
-                    self._check_type(ak["info"]["head"], str, "info/head", item=item)
-                    self._check_type(
-                        ak["info"]["description"], str, "info/description", item=item
-                    )
-                    self._check_type(ak["info"]["reso"], bool, "info/reso", item=item)
-                    self._check_type(
-                        ak["info"]["duration_in_hours"],
-                        float,
-                        "info/duration_in_hours",
-                        item=item,
-                    )
-                    self._check_type(
-                        ak["info"]["django_ak_id"],
-                        int,
-                        "info/django_ak_id",
-                        item=item,
-                    )
-
-                    self._check_lst(
-                        ak["properties"]["conflicts"],
-                        "conflicts",
-                        item=item,
-                        contained_type=int,
-                    )
-                    self._check_lst(
-                        ak["properties"]["dependencies"],
-                        "dependencies",
-                        item=item,
-                        contained_type=int,
-                    )
-                    self._check_lst(
-                        ak["time_constraints"], "time_constraints", item=item
-                    )
-                    self._check_lst(
-                        ak["room_constraints"], "room_constraints", item=item
-                    )
+                error = best_match(
+                    self.json_export_validator.iter_errors(self.export_dict)
+                )
+                msg = "" if error is None else error.message
+                self.assertTrue(error is None, msg)
 
-    def test_room_conformity_to_spec(self):
-        """Test if Room JSON structure and types conform to standard."""
+    def test_id_uniqueness(self):
+        """Test if objects are only exported once."""
         for event in Event.objects.all():
             with self.subTest(event=event):
                 self.set_up_event(event=event)
 
-                self._check_uniqueness(self.export_dict["rooms"], "Room")
-                for room in self.export_dict["rooms"]:
-                    item = f"Room {room['id']}"
-                    self.assertEqual(
-                        room.keys(),
-                        {
-                            "id",
-                            "info",
-                            "capacity",
-                            "fulfilled_room_constraints",
-                            "time_constraints",
-                        },
-                        f"{item} keys not as expected",
-                    )
-                    self.assertEqual(
-                        room["info"].keys(),
-                        {"name"},
-                        f"{item} info keys not as expected",
-                    )
-
-                    self._check_type(room["id"], int, "id", item=item)
-                    self._check_type(room["capacity"], int, "capacity", item=item)
-                    self._check_type(room["info"]["name"], str, "info/name", item=item)
-
-                    self.assertTrue(
-                        room["capacity"] > 0 or room["capacity"] == -1,
-                        "invalid room capacity",
-                    )
-
-                    self._check_lst(
-                        room["time_constraints"], "time_constraints", item=item
-                    )
-                    self._check_lst(
-                        room["fulfilled_room_constraints"],
-                        "fulfilled_room_constraints",
-                        item=item,
-                    )
-
-    def test_timeslots_conformity_to_spec(self):
-        """Test if Timeslots JSON structure and types conform to standard."""
-        for event in Event.objects.all():
-            with self.subTest(event=event):
-                self.set_up_event(event=event)
+                self._check_uniqueness(self.export_dict["aks"], "AKs")
+                self._check_uniqueness(self.export_dict["rooms"], "Rooms")
+                self._check_uniqueness(self.export_dict["participants"], "Participants")
 
                 self._check_uniqueness(
                     chain.from_iterable(self.export_dict["timeslots"]["blocks"]),
                     "Timeslots",
                 )
-                item = "timeslots"
-                self.assertEqual(
-                    self.export_dict["timeslots"].keys(),
-                    {"info", "blocks"},
-                    "timeslot keys not as expected",
-                )
-                self.assertEqual(
-                    self.export_dict["timeslots"]["info"].keys(),
-                    {"duration", "blocknames"},
-                    "timeslot info keys not as expected",
-                )
-                self._check_type(
-                    self.export_dict["timeslots"]["info"]["duration"],
-                    float,
-                    "info/duration",
-                    item=item,
-                )
-                self._check_lst(
-                    self.export_dict["timeslots"]["info"]["blocknames"],
-                    "info/blocknames",
-                    item=item,
-                    contained_type=list,
-                )
-                for blockname in self.export_dict["timeslots"]["info"]["blocknames"]:
-                    self.assertEqual(len(blockname), 2)
-                    self._check_lst(
-                        blockname,
-                        "info/blocknames/entry",
-                        item=item,
-                        contained_type=str,
-                    )
 
-                self._check_lst(
-                    self.export_dict["timeslots"]["blocks"],
-                    "blocks",
-                    item=item,
-                    contained_type=list,
-                )
+    def test_timeslot_ids_consecutive(self):
+        """Test if Timeslots ids are chronologically consecutive."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
 
                 prev_id = None
                 for timeslot in chain.from_iterable(
                     self.export_dict["timeslots"]["blocks"]
                 ):
-                    item = f"timeslot {timeslot['id']}"
-                    self.assertEqual(
-                        timeslot.keys(),
-                        {"id", "info", "fulfilled_time_constraints"},
-                        f"{item} keys not as expected",
-                    )
-                    self.assertEqual(
-                        timeslot["info"].keys(),
-                        {"start", "end"},
-                        f"{item} info keys not as expected",
-                    )
-                    self._check_type(timeslot["id"], int, "id", item=item)
-                    self._check_type(
-                        timeslot["info"]["start"], str, "info/start", item=item
-                    )
-                    self._check_lst(
-                        timeslot["fulfilled_time_constraints"],
-                        "fulfilled_time_constraints",
-                        item=item,
-                    )
-
                     if prev_id is not None:
                         self.assertLess(
                             prev_id,
@@ -351,8 +187,6 @@ class JSONExportTest(TestCase):
                         getattr(self.event, attr_field), self.export_dict["info"][attr]
                     )
 
-                self._check_uniqueness(self.export_dict["participants"], "Participants")
-
     def test_ak_durations(self):
         """Test if all AK durations are correct."""
         for event in Event.objects.all():
diff --git a/AKModel/utils.py b/AKModel/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a0b3f0d57cb646954cd34a9a76c5903ee74cc10
--- /dev/null
+++ b/AKModel/utils.py
@@ -0,0 +1,27 @@
+import json
+from pathlib import Path
+
+from jsonschema import Draft202012Validator
+from jsonschema.protocols import Validator
+from referencing import Registry, Resource
+
+from AKPlanning import settings
+
+
+def construct_schema_validator(schema: str | dict) -> Validator:
+    """Construct a validator for a JSON schema.
+
+    In particular, all schemas from the 'schemas' directory
+    are loaded into the registry.
+    """
+    schema_base_path = Path(settings.BASE_DIR) / "schemas"
+    resources = []
+    for schema_path in schema_base_path.glob("**/*.schema.json"):
+        with schema_path.open("r") as ff:
+            res = Resource.from_contents(json.load(ff))
+        resources.append((res.id(), res))
+    registry = Registry().with_resources(resources)
+    if isinstance(schema, str):
+        with (schema_base_path / schema).open("r") as ff:
+            schema = json.load(ff)
+    return Draft202012Validator(schema=schema, registry=registry)
diff --git a/requirements.txt b/requirements.txt
index 451feef4db2ee295a38687bc0f96ff1dee637418..3183a66eee179f45bba2314a2efff72b68a7e865 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,6 +16,7 @@ django-libsass==0.9
 django-betterforms==2.0.0
 mysqlclient==2.2.0  # for production deployment
 tzdata==2024.1
+jsonschema==4.23.0
 
 # Documentation
 sphinxcontrib-django==2.5
diff --git a/schemas/ak.schema.json b/schemas/ak.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..a7428a30ff6ce7e47c8650ec6de66e80112a2b4c
--- /dev/null
+++ b/schemas/ak.schema.json
@@ -0,0 +1,62 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/ak.schema.json",
+  "title": "AK",
+  "type": "object",
+  "properties": {
+    "id": {
+      "$ref": "/schema/common/id.schema.json",
+      "description": "The unique identifier of an AK"
+    },
+    "duration": {
+      "description": "The number of consecutive slot units",
+      "type": "integer",
+      "exclusiveMinimum": 0
+    },
+    "room_constraints": {
+      "$ref": "/schema/common/constraints.schema.json",
+      "description": "Room constraints required by this AK"
+    },
+    "time_constraints": {
+      "$ref": "/schema/common/constraints.schema.json",
+      "description": "Time constraints required by this AK"
+    },
+    "properties": {
+      "type": "object",
+      "properties": {
+        "conflicts": {
+          "$ref": "/schema/common/id_array.schema.json",
+          "description": "IDs of all AKs that are in conflict with this AK"
+        },
+        "dependencies": {
+          "$ref": "/schema/common/id_array.schema.json",
+          "description": "IDs of all AKs that should be scheduled before this AK"
+        }
+      },
+      "required": ["conflicts", "dependencies"],
+      "additionalProperties": false
+    },
+    "info": {
+      "type": "object",
+      "properties": {
+        "name": {"description": "Name of the AK", "type": "string"},
+        "head": {"description": "Name of the head of the AK", "type": "string"},
+        "description": {"description": "Short description of the AK", "type": "string"},
+        "reso": {"description": "Whether this AK intends to introduce a resolution", "type": "boolean"},
+        "duration_in_hours": {"description": "AK duration in hours", "type": "number"},
+        "django_ak_id": {
+          "$ref": "/schema/common/id.schema.json",
+          "description": "Unique identifier of the AK object in the django database"
+        },
+        "types": {
+          "$ref": "/schema/common/constraints.schema.json",
+          "description": "Types of this AK"
+        }
+      },
+      "required": ["name", "head", "description", "reso", "duration_in_hours", "django_ak_id", "types"],
+      "additionalProperties": false
+    }
+  },
+  "required": ["id", "duration", "room_constraints", "time_constraints", "properties", "info"],
+  "additionalProperties": false
+}
diff --git a/schemas/common/constraints.schema.json b/schemas/common/constraints.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..7d3fce56e26ba2403538583c20a0d211b56c2774
--- /dev/null
+++ b/schemas/common/constraints.schema.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/common/constraints.schema.json",
+  "type": "array",
+  "items": {"type": "string"},
+  "uniqueItems": true
+}
\ No newline at end of file
diff --git a/schemas/common/id.schema.json b/schemas/common/id.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..4dd06e349a9ec29caf5cf051d9044a9e47a2924d
--- /dev/null
+++ b/schemas/common/id.schema.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/common/id.schema.json",
+  "type": "integer",
+  "minimum": 0
+}
\ No newline at end of file
diff --git a/schemas/common/id_array.schema.json b/schemas/common/id_array.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..622e692ac617ed4fc5ce8261b9da135a0ab1e79d
--- /dev/null
+++ b/schemas/common/id_array.schema.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/common/id_array.schema.json",
+  "type": "array",
+  "items": {"type": "integer"},
+  "uniqueItems": true
+}
\ No newline at end of file
diff --git a/schemas/participant.schema.json b/schemas/participant.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..a7eff8e8f87534d4a50b7789c82a07f6163ebb98
--- /dev/null
+++ b/schemas/participant.schema.json
@@ -0,0 +1,58 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/participant.schema.json",
+  "title": "Participant",
+  "type": "object",
+  "properties": {
+    "id": {
+      "$ref": "/schema/common/id.schema.json",
+      "description": "The unique identifier of a participant"
+    },
+    "preferences": {
+        "description": "AK preferences of the participant",
+        "type": "array",
+        "items": {
+          "type": "object",
+          "properties": {
+            "ak_id": {
+              "$ref": "/schema/common/id.schema.json",
+              "description": "The unique identifier of the AK"
+            },
+            "required": {
+              "type": "boolean",
+              "description": "whether this participant is required for the AK"
+            },
+            "preference_score": {
+              "type": "integer",
+              "description": "The prefeference score for this AK",
+              "default": 0,
+              "minimum": -1,
+              "maximum": 2,
+              "anyOf": [
+                {"const": -1}, {"const": 1}, {"const": 2}
+              ]
+            }
+          },
+          "required": ["ak_id", "required", "preference_score"],
+          "additionalProperties": false
+        },
+        "uniqueItems": true
+    },
+    "room_constraints": {
+      "$ref": "/schema/common/constraints.schema.json",
+      "description": "Room constraints required by this participant"
+    },
+    "time_constraints": {
+      "$ref": "/schema/common/constraints.schema.json",
+      "description": "Time constraints required by this participant"
+    },
+    "info": {
+      "type": "object",
+      "properties": {"name": {"description": "Name of the person", "type": "string"}},
+      "required": ["name"],
+      "additionalProperties": false
+    }
+  },
+  "required": ["id", "room_constraints", "time_constraints", "info"],
+  "additionalProperties": false
+}
diff --git a/schemas/room.schema.json b/schemas/room.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..a4ab82df9e157cc2d368ec1813ea601c1f96cbe4
--- /dev/null
+++ b/schemas/room.schema.json
@@ -0,0 +1,37 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/room.schema.json",
+  "title": "Room",
+  "type": "object",
+  "properties": {
+    "id": {
+      "$ref": "/schema/common/id.schema.json",
+      "description": "The unique identifier of a room"
+    },
+    "capacity": {
+      "description": "The maximum number of total participants. Unbounded capacity is represented by -1",
+      "type": "integer",
+      "anyOf": [
+        {"minimum": 1}, {"const": -1}
+      ]
+    },
+    "fulfilled_room_constraints": {
+      "$ref": "/schema/common/constraints.schema.json",
+      "description": "Constraints fulfilled by this room"
+    },
+    "time_constraints": {
+      "$ref": "/schema/common/constraints.schema.json",
+      "description": "Time constraints required by this room"
+    },
+    "info": {
+      "type": "object",
+      "properties": {
+        "name": {"description": "Name of the room", "type": "string"}
+      },
+      "required": ["name"],
+      "additionalProperties": false
+    }
+  },
+  "required": ["id", "capacity", "fulfilled_room_constraints", "time_constraints", "info"],
+  "additionalProperties": false
+}
\ No newline at end of file
diff --git a/schemas/solver-input.schema.json b/schemas/solver-input.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..a55057dba34c514435a46e991b3671eb2f289d5e
--- /dev/null
+++ b/schemas/solver-input.schema.json
@@ -0,0 +1,23 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/solver-input.schema.json",
+  "type": "object",
+  "properties": {
+      "aks": {"type": "array", "items": {"$ref": "/schema/ak.schema.json"}, "uniqueItems": true},
+      "rooms": {"type": "array", "items": {"$ref": "/schema/room.schema.json"}, "uniqueItems": true},
+      "participants": {"type": "array", "items": {"$ref": "/schema/participant.schema.json"}, "uniqueItems": true},
+      "timeslots": {"$ref": "/schema/timeslot.schema.json"},
+      "info": {
+        "type": "object",
+        "properties": {
+          "title": {"type": "string"},
+          "slug": {"type": "string"},
+          "place": {"type": "string"},
+          "contact_email": {"type": "string"}
+        },
+        "additionalProperties": false
+      }
+  },
+  "required": ["aks", "rooms", "participants", "timeslots", "info"],
+  "additionalProperties": false
+}
diff --git a/schemas/solver-output.schema.json b/schemas/solver-output.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..2a15247f85c1b13a8b08a48e4fe5cb59ea30bfc9
--- /dev/null
+++ b/schemas/solver-output.schema.json
@@ -0,0 +1,52 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/solver-output.schema.json",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["input", "scheduled_aks"],
+  "properties": {
+    "input": {"$ref": "/schema/solver-input.schema.json"},
+    "scheduled_aks": {
+      "type": "array",
+      "items": {
+        "description": "An object representing the scheduling information for one AK",
+        "type": "object",
+        "properties": {
+          "ak_id": {
+            "description": "The unique identifier of the scheduled AK",
+            "type": "integer",
+            "minimum": 0
+          },
+          "room_id": {
+            "description": "The unique identifier of the room the AK takes place in",
+            "type": "integer",
+            "minimum": 0
+          },
+          "timeslot_ids": {
+            "description": "The unique identifiers of all timeslots the AK takes place in",
+            "type": "array",
+            "items": {
+              "description": "The unique identifier of the referenced timeslot",
+              "type": "integer",
+              "minimum": 0
+            },
+            "uniqueItems": true
+          },
+          "participant_ids": {
+            "description": "The unique identifiers of all participants assigned to the AK",
+            "type": "array",
+            "items": {
+              "description": "The unique identifier of the referenced participant",
+              "type": "integer",
+              "minimum": 0
+            },
+            "uniqueItems": true
+          }
+        },
+        "required": ["ak_id", "room_id", "timeslot_ids", "participant_ids"],
+        "additionalProperties": false
+      },
+      "uniqueItems": true
+    }
+  }
+}
diff --git a/schemas/timeslot.schema.json b/schemas/timeslot.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..0240c4969741e5d5934938c7000566ef64889f13
--- /dev/null
+++ b/schemas/timeslot.schema.json
@@ -0,0 +1,61 @@
+ {
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "/schema/timeslot.schema.json",
+  "title": "Timeslot",
+  "type": "object",
+  "required": ["info", "blocks"],
+  "additionalProperties": false,
+  "properties": {
+    "info": {
+      "type": "object",
+      "properties": {
+        "duration": {"description": "Duration in hours of a slot unit", "type": "number"},
+        "blocknames": {
+          "type": "array",
+          "items": {
+            "type": "array",
+            "items": {"type": "string"},
+            "minItems": 2,
+            "maxItems": 2
+          }
+        }
+      },
+      "required": ["duration"],
+      "additionalProperties": false
+    },
+    "blocks": {
+      "type": "array",
+      "description": "Blocks of consecutive timeslots",
+      "items": {
+        "type": "array",
+        "description": "A single block of consecutive timeslots",
+        "items": {
+          "type": "object",
+          "description": "A single timeslot",
+          "properties": {
+            "id": {
+              "$ref": "/schema/common/id.schema.json",
+              "description": "The unique identifier of the single timeslot. Accross all blocks, the ids must be sorted chronologically."
+            },
+            "info": {
+              "type": "object",
+              "properties": {
+                "start": {"description": "Start datetime of the timeslot", "type": "string"},
+                "end": {"description": "End datetime of the timeslot", "type": "string"}
+              },
+              "required": ["start", "end"],
+              "additionalProperties": false
+            },
+            "fulfilled_time_constraints": {
+              "$ref": "/schema/common/constraints.schema.json",
+              "description": "Time constraints fulfilled by this timeslot"
+            }
+          },
+          "required": ["id", "info", "fulfilled_time_constraints"],
+          "additionalProperties": false
+        }
+      }
+    }
+  }
+}
+ 
\ No newline at end of file