diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index e8aac8fed36ba4f6917c3e22e87ae2ebec739775..78046fc9617e2810d24d7d962c02745d851f567a 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-export.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 not error else f"{error.message} at {error.json_path}"
+                self.assertFalse(error, 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..a7b5d2130d07ad046c36dc1790a94c40efe6d173
--- /dev/null
+++ b/AKModel/utils.py
@@ -0,0 +1,43 @@
+from pathlib import Path
+
+import referencing.retrieval
+from jsonschema import Draft202012Validator
+from jsonschema.protocols import Validator
+from referencing import Registry
+
+from AKPlanning import settings
+
+
+def _construct_schema_path(uri: str | Path) -> Path:
+    """Construct a schema URI.
+
+    This function also checks for unallowed directory traversals
+    out of the 'schema' subfolder.
+    """
+    schema_base_path = Path(settings.BASE_DIR).resolve()
+    uri_path = (schema_base_path / uri).resolve()
+    if not uri_path.is_relative_to(schema_base_path / "schemas"):
+        raise ValueError("Unallowed dictionary traversal")
+    return uri_path
+
+
+@referencing.retrieval.to_cached_resource()
+def retrieve_schema_from_disk(uri: str) -> str:
+    """Retrieve schemas from disk by URI."""
+    uri_path = _construct_schema_path(uri)
+    with uri_path.open("r") as ff:
+        return ff.read()
+
+
+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.
+    """
+    registry = Registry(retrieve=retrieve_schema_from_disk)
+
+    if isinstance(schema, str):
+        schema_uri = str(Path("schemas") / schema)
+        schema = registry.get_or_retrieve(schema_uri).value.contents
+    return Draft202012Validator(schema=schema, registry=registry)
diff --git a/requirements.txt b/requirements.txt
index 9b0938d4a76aff97222757ebd6ac30ac3efef24a..dc320c25b5f22c90f4b77e72f99c23e1e99b7513 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -17,6 +17,7 @@ djangorestframework==3.15.2
 fontawesomefree==6.6.0  # Makes static files (css, fonts) available locally
 mysqlclient==2.2.7  # for production deployment
 tzdata==2025.1
+jsonschema==4.23.0
 
 # Documentation
 Sphinx==8.2.3
diff --git a/schemas/ak-export.schema.json b/schemas/ak-export.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..b4c8d63cdee632018ab7b6a58919e45a9ef1ff63
--- /dev/null
+++ b/schemas/ak-export.schema.json
@@ -0,0 +1,10 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/ak-export.schema.json",
+  "properties": {
+    "info": {
+      "$ref": "ak.schema.json#/properties/info",
+      "unevaluatedProperties": false
+    }
+  }
+}
diff --git a/schemas/ak.schema.json b/schemas/ak.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..a8578a347cf78c337f7c99f15942cf268fe1576a
--- /dev/null
+++ b/schemas/ak.schema.json
@@ -0,0 +1,61 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/ak.schema.json",
+  "title": "AK",
+  "type": "object",
+  "properties": {
+    "id": {
+      "$ref": "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": "common/constraints.schema.json",
+      "description": "Room constraints required by this AK"
+    },
+    "time_constraints": {
+      "$ref": "common/constraints.schema.json",
+      "description": "Time constraints required by this AK"
+    },
+    "properties": {
+      "type": "object",
+      "properties": {
+        "conflicts": {
+          "$ref": "common/id_array.schema.json",
+          "description": "IDs of all AKs that are in conflict with this AK"
+        },
+        "dependencies": {
+          "$ref": "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": "common/id.schema.json",
+          "description": "Unique identifier of the AK object in the django database"
+        },
+        "types": {
+          "$ref": "common/constraints.schema.json",
+          "description": "Types of this AK"
+        }
+      },
+      "required": ["name", "head", "description", "reso", "duration_in_hours", "django_ak_id", "types"]
+    }
+  },
+  "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..21c0dcd661673da3e90d795df2c77cc700905808
--- /dev/null
+++ b/schemas/common/constraints.schema.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/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..636d6c709c54d425d94ef9dbee8c28d232e3319b
--- /dev/null
+++ b/schemas/common/id.schema.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/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..322fcb6ba76d8c073190d6ac6825fd834e8e8b7a
--- /dev/null
+++ b/schemas/common/id_array.schema.json
@@ -0,0 +1,7 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/common/id_array.schema.json",
+  "type": "array",
+  "items": {"type": "integer"},
+  "uniqueItems": true
+}
\ No newline at end of file
diff --git a/schemas/participant-export.schema.json b/schemas/participant-export.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..d47a33c29bfbe4b3278feda411d7786ebb873254
--- /dev/null
+++ b/schemas/participant-export.schema.json
@@ -0,0 +1,22 @@
+
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/participant-export.schema.json",
+  "properties": {
+    "preferences": {
+        "items": {
+          "properties": {
+            "preference_score": {
+              "anyOf": [
+                {"const": -1}, {"const": 1}, {"const": 2}
+              ]
+            }
+          }
+        }
+    },
+    "info": {
+      "$ref": "participant.schema.json#/properties/info",
+      "unevaluatedProperties": false
+    }
+  }
+}
diff --git a/schemas/participant.schema.json b/schemas/participant.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..70631eb10d3a347e0412fe1ba4adb4c1c7d9035b
--- /dev/null
+++ b/schemas/participant.schema.json
@@ -0,0 +1,54 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/participant.schema.json",
+  "title": "Participant",
+  "type": "object",
+  "properties": {
+    "id": {
+      "$ref": "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": "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
+            }
+          },
+          "required": ["ak_id", "required", "preference_score"],
+          "additionalProperties": false
+        },
+        "uniqueItems": true
+    },
+    "room_constraints": {
+      "$ref": "common/constraints.schema.json",
+      "description": "Room constraints required by this participant"
+    },
+    "time_constraints": {
+      "$ref": "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"]
+    }
+  },
+  "required": ["id", "room_constraints", "time_constraints", "info"],
+  "additionalProperties": false
+}
diff --git a/schemas/room-export.schema.json b/schemas/room-export.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..8942a2d383b30f7aac1806239c2bdc9a4b9fb92d
--- /dev/null
+++ b/schemas/room-export.schema.json
@@ -0,0 +1,10 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/room-export.schema.json",
+  "properties": {
+    "info": {
+      "$ref": "room.schema.json#/properties/info",
+      "unevaluatedProperties": false
+    }
+  }
+}
diff --git a/schemas/room.schema.json b/schemas/room.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..dbf331f124f6e96fc05c2c533ed34a30105417fb
--- /dev/null
+++ b/schemas/room.schema.json
@@ -0,0 +1,36 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/room.schema.json",
+  "title": "Room",
+  "type": "object",
+  "properties": {
+    "id": {
+      "$ref": "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": "common/constraints.schema.json",
+      "description": "Constraints fulfilled by this room"
+    },
+    "time_constraints": {
+      "$ref": "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"]
+    }
+  },
+  "required": ["id", "capacity", "fulfilled_room_constraints", "time_constraints", "info"],
+  "additionalProperties": false
+}
\ No newline at end of file
diff --git a/schemas/solver-input-export.schema.json b/schemas/solver-input-export.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..6fbf48a7ef01da9dbc19cdb719e3a3bed6ae2355
--- /dev/null
+++ b/schemas/solver-input-export.schema.json
@@ -0,0 +1,15 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/solver-input-export.schema.json",
+  "allOf": [{ "$ref": "solver-input.schema.json"}],
+  "properties": {
+    "participants": {"items": {"$ref": "participant-export.schema.json"}},
+    "rooms": {"items": {"$ref": "room-export.schema.json"}},
+    "timeslots": {"items": {"$ref": "timeslot-export.schema.json"}},
+    "aks": {"items": {"$ref": "ak-export.schema.json"}},
+    "info": {
+      "$ref": "solver-input.schema.json#/properties/info",
+      "unevaluatedProperties": 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..faece29b0b2bbf402798646d2d112e74a778e9ca
--- /dev/null
+++ b/schemas/solver-input.schema.json
@@ -0,0 +1,22 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/solver-input.schema.json",
+  "type": "object",
+  "properties": {
+      "aks": {"type": "array", "items": {"$ref": "ak.schema.json"}, "uniqueItems": true},
+      "rooms": {"type": "array", "items": {"$ref": "room.schema.json"}, "uniqueItems": true},
+      "participants": {"type": "array", "items": {"$ref": "participant.schema.json"}, "uniqueItems": true},
+      "timeslots": {"$ref": "timeslot.schema.json"},
+      "info": {
+        "type": "object",
+        "properties": {
+          "title": {"type": "string"},
+          "slug": {"type": "string"},
+          "place": {"type": "string"},
+          "contact_email": {"type": "string"}
+        }
+      }
+  },
+  "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..c4a8868af02242e9e1fc789c812ac928eb520b31
--- /dev/null
+++ b/schemas/solver-output.schema.json
@@ -0,0 +1,52 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/solver-output.schema.json",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["input", "scheduled_aks"],
+  "properties": {
+    "input": {"$ref": "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-export.schema.json b/schemas/timeslot-export.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..1689ad38add2c48e0b4be5a0764f0f9e65bc815f
--- /dev/null
+++ b/schemas/timeslot-export.schema.json
@@ -0,0 +1,20 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/timeslot-export.schema.json",
+  "properties": {
+    "info": {
+      "$ref": "timeslot.schema.json#/properties/info",
+      "unevaluatedProperties": false
+    },
+    "blocks": {
+      "items": {
+        "items": {
+          "info": {
+            "$ref": "timeslot.schema.json#/properties/blocks/items/items/info",
+            "unevaluatedProperties": false
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/schemas/timeslot.schema.json b/schemas/timeslot.schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..2cd23cd0fc877c2d4ce05b11a87b797f9299083c
--- /dev/null
+++ b/schemas/timeslot.schema.json
@@ -0,0 +1,59 @@
+ {
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "schemas/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"]
+    },
+    "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": "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"]
+            },
+            "fulfilled_time_constraints": {
+              "$ref": "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