Authoritative reference for export/import compatibility
This document specifies the on-disk format used by Open Tests for exporting and importing tests as a single ZIP archive. It is intended for developers who want to generate compatible ZIPs or process exported tests.
All JSON files MUST be UTF-8 encoded. Paths inside JSON MUST use forward slashes (/).
A valid archive MUST contain:
test_settings.json – Top-level test metadata and behavior flags.questions/ – Folder containing one JSON file per question.Optional entries:
assets/ – Folder containing media referenced by questions and options.Example structure:
my-test.zip
└── /
├── test_settings.json
├── questions/
│ ├── 001.json
│ ├── 002.json
│ └── 003.json
├── assets/
│ ├── q001/
│ │ ├── question.png
│ │ └── option_1.webp
│ └── q002/
│ └── left_1.jpg
Notes:
- File names inside questions/ are not strictly prescribed; any .json files in this folder are considered. Zero-padded numeric prefixes are RECOMMENDED to preserve order for humans.
- The assets/ folder and its inner layout are not strictly prescribed; media are referenced by relative paths under assets/.
test_settings.jsonTop-level test configuration. Example:
{
"title": "Podstawy JavaScriptu",
"description": "Test wprowadzający do JS",
"allowScrolling": false,
"showAnswerAfterQuestion": false,
"showAnswersAtEnd": true,
"randomizeQuestions": false,
"randomizeAnswers": false,
"timeLimit": 30,
"passThreshold": 60
}
Field reference:
title (string, required): Human-readable name. Non-empty on export.description (string, optional): Free text.allowScrolling (boolean, required): Allow vertical scrolling between questions in a single view.showAnswerAfterQuestion (boolean, required): Reveal correct answers after each question.showAnswersAtEnd (boolean, required): Reveal correct answers after finishing the test.randomizeQuestions (boolean, required): Shuffle question order.randomizeAnswers (boolean, required): Shuffle options within questions that have options.timeLimit (integer|null, optional): Time limit in MINUTES for the entire test. null or omitted means no limit.passThreshold (integer|null, optional): Minimum percentage (0–100). null or omitted means no threshold.questions/*.json)Each file in questions/ describes a single question. Example (common shape):
{
"id": 1,
"type": "single-choice",
"content": "Który operator służy do konkatenacji stringów w JS?",
"media": "assets/q001/question.png",
"timeLimit": 45,
"maxPoints": 1,
"typeSpecificData": { /* varies by type */ }
}
Common fields:
id (integer, required): Question identifier. On import, IDs may be normalized to sequential values in the UI; keep values unique.type (string, required): One of single-choice, multiple-choice, ordering, matching, short-answer, open-question.content (string, required): The question text.media (string|absent, optional): Relative path under assets/ to an image or video associated with the question.timeLimit (integer|null, optional): Per-question time limit in SECONDS. null/omitted means no limit.maxPoints (number, required): Positive value (e.g., 1, 0.5, 2.0).typeSpecificData (object, required): Payload that depends on type (see below).Wherever a media field appears (question-level or inside options/items/pairs), the exported JSON stores a STRING path relative to the archive root, starting with assets/. Example: "assets/q001/option_1.webp".
Supported formats (by MIME/extension):
image/jpeg (.jpg, .jpeg), image/png (.png), image/gif (.gif), image/webp (.webp), image/svg+xml (.svg).video/mp4 (.mp4), video/webm (.webm).Guidelines:
Below are precise structures for typeSpecificData per question type. Unspecified additional fields SHOULD NOT be used.
single-choice{
"options": [
{ "id": 1, "text": "+", "media": "assets/q001/option_1.webp" },
{ "id": 2, "text": "&", "media": null },
{ "id": 3, "text": "||" }
],
"correctOptionId": 1
}
options (array, required): List of selectable options. Each option: { id: integer, text: string, media?: string }.correctOptionId (integer, required): Must equal one of the options[].id.Constraints:
multiple-choice{
"options": [
{ "id": 1, "text": "Array", "media": null },
{ "id": 2, "text": "Object" },
{ "id": 3, "text": "Map" }
],
"correctOptionIds": [2, 3],
"partialCredit": true
}
options (array, required): Same shape as single-choice options.correctOptionIds (array<int>, required): One or more option IDs from options[].id.partialCredit (boolean, required): Award partial credit for partially correct selections.Constraints:
correctOptionIds.length >= 1.ordering{
"items": [
{ "id": 1, "text": "Pobierz dane", "media": null },
{ "id": 2, "text": "Przetwórz dane" },
{ "id": 3, "text": "Wyświetl wynik" }
]
}
items (array, required): Items listed in the CORRECT order. Each item: { id: integer, text: string, media?: string }.Constraints:
matching{
"pairs": [
{
"id": 1,
"left": { "text": "HTML", "media": null },
"right": { "text": "Struktura", "media": null }
},
{
"id": 2,
"left": { "text": "CSS" },
"right": { "text": "Prezentacja" }
}
]
}
pairs (array, required): Correctly matched left/right items.{ id: integer, left: { text: string, media?: string }, right: { text: string, media?: string } }.Constraints:
short-answer{
"correctAnswers": ["ECMAScript", "ES"],
"exactMatch": false
}
correctAnswers (array<string>, required): Acceptable answers.exactMatch (boolean, required): If true, user answer must match exactly (case sensitivity is implementation-defined; creators should normalize where relevant).Constraints:
open-question{
"gradingKey": "Oceń poprawność koncepcji, przykład i uzasadnienie."
}
gradingKey (string, required): Rubric/instructions for manual grading.title non-empty string.passThreshold either null or integer in [0, 100].timeLimit either null or positive integer (minutes).id unique within the archive; positive integer recommended.type is one of the supported values.content non-empty string.maxPoints positive number (> 0).media strings must point under assets/ if present.This section defines explicit min/max limits for numeric fields and string lengths. Unless specified otherwise, strings refer to character counts and integers are base-10 whole numbers.
title (string): length 1–200.description (string): length 0–2000.allowScrolling, showAnswerAfterQuestion, showAnswersAtEnd, randomizeQuestions, randomizeAnswers (boolean): must be boolean.timeLimit (integer|null, minutes): null or 1–1440.passThreshold (integer|null, percent): null or 0–100.id (integer): 1–2,147,483,647; unique within archive.type (enum string): one of the supported values.content (string): length 1–2000.media (string path): if present, length 1–300 and must start with assets/.timeLimit (integer|null, seconds): null or 5–3600.maxPoints (number): 0.01–100.00; up to 2 decimal places.typeSpecificData
options (array): 2–20 items.options[].id (integer): 1–2,147,483,647; unique within the question.options[].text (string): length 1–200.options[].media (string path): if present, length 1–300 and must start with assets/.correctOptionId (integer): must equal one of options[].id.typeSpecificData
options (array): 2–20 items (same per-option limits as single-choice).correctOptionIds (array<int>): length 1–options.length; values unique; each must exist in options[].id.partialCredit (boolean): must be boolean.typeSpecificData
items (array): 2–20 items.items[].id (integer): 1–2,147,483,647; unique within the question.items[].text (string): length 1–200.items[].media (string path): if present, length 1–300 and must start with assets/.typeSpecificData
pairs (array): 1–20 pairs.pairs[].id (integer): 1–2,147,483,647; unique within the question.left.text / right.text (string): length 1–200 each.left.media / right.media (string path): if present, length 1–300 and must start with assets/.typeSpecificData
correctAnswers (array<string>): 1–50 items.correctAnswers[] (string): length 1–500.exactMatch (boolean): must be boolean.typeSpecificData
gradingKey (string): length 1–2000.assets/.questions/ may be named with numeric prefixes for readability, but the importer does not require a specific naming convention.id values unique to avoid ambiguity.All text fields (title, description, content, option texts, grading keys, etc.) support arbitrary UTF-8 content, including diacritics and RTL scripts.
minimal.zip
└── /
├── test_settings.json
└── questions/
└── 001.json
test_settings.json:
{
"title": "Minimalny test",
"description": "",
"allowScrolling": false,
"showAnswerAfterQuestion": false,
"showAnswersAtEnd": true,
"randomizeQuestions": false,
"randomizeAnswers": false,
"timeLimit": null,
"passThreshold": null
}
questions/001.json:
{
"id": 1,
"type": "single-choice",
"content": "1 + 1 = ?",
"maxPoints": 1,
"typeSpecificData": {
"options": [
{ "id": 1, "text": "2" },
{ "id": 2, "text": "3" }
],
"correctOptionId": 1
}
}
test_settings.timeLimit is in MINUTES, while question.timeLimit is in SECONDS.assets/ is optional; absence of assets/ is valid if no media fields are present.