Examplary
  • Start for free
    Developer docs/Question types

    QTI interchange

    Examplary supports importing and exporting questions in QTI 3.0 format.

    By default, it will run your custom assessment component in a QTI-compatible way, but you can also define custom mappings for your question types. This is great if your question type implements a default interaction type supported in the QTI spec, or if you want to customize how your question data is represented in QTI.

    This is done through the interchange field in your question-type.json file, using JSONata expressions to define the mapping between your question settings and QTI elements.

    Basic structure

    The interchange configuration lives under the interchange.qti3 key:

    question-type.json
    {
      "interchange": {
        "qti3": {
          "interactionType": "choiceInteraction",
          "export": { ... },
          "import": { ... }
        }
      }
    }
    FieldRequiredDescription
    interactionTypeYesThe QTI interaction type (e.g., choiceInteraction, extendedTextInteraction)
    exportNoConfiguration for exporting questions to QTI
    importNoConfiguration for importing QTI interactions as questions
    Tip

    You can define just export, just import, or both. At least one must be present.

    JSONata expressions

    All dynamic values in the interchange configuration are JSONata expressions. JSONata is a lightweight query and transformation language for JSON data.

    Export context

    During export, the question object is available as $question:

    {
      "id": "q_abc123",
      "title": "What is the capital of France?",
      "description": "Select the correct answer.",
      "settings": {
        "options": [
          { "text": "London", "correct": false },
          { "text": "Paris", "correct": true },
          { "text": "Berlin", "correct": false }
        ],
        "shuffleOptions": true,
        "maxSelections": 1
      }
    }

    Access properties using $question.settings.shuffleOptions, $question.description, etc.

    Import context

    During import, the parsed QTI interaction is available as $interaction:

    {
      "type": "choiceInteraction",
      "attributes": {
        "shuffle": true,
        "max-choices": 1
      },
      "prompt": "Select the correct answer.",
      "choices": [
        { "identifier": "A", "content": "London" },
        { "identifier": "B", "content": "Paris" },
        { "identifier": "C", "content": "Berlin" }
      ],
      "correctResponse": ["B"]
    }

    Access properties using $interaction.prompt, $interaction.choices, etc.

    Common JSONata patterns

    Here are some JSONata patterns you'll use frequently:

    // Simple property access
    $question.settings.shuffleOptions           // → true
     
    // Conditional (ternary)
    $question.settings.maxSelections = 1 ? 'single' : 'multiple'  // → "single"
     
    // String concatenation
    'choice_' & $string($index)       // → "choice_0", "choice_1", etc.
     
    // Filter array by property
    $question.settings.options[correct = true]  // → [{ "text": "Paris", "correct": true }]
     
    // Map array to new structure
    $question.settings.options.(text)           // → ["London", "Paris", "Berlin"]
     
    // Check if value is in array (import)
    identifier in $interaction.correctResponse  // → true or false
     
    // Get array indices of matching items
    $question.settings.options[correct].$index  // → [1] (indices where correct is true)
    Learn more

    For comprehensive JSONata documentation, visit jsonata.org. You can also use the JSONata Exerciser to test your expressions interactively.

    Export configuration

    The export configuration defines how to transform your question into QTI XML.

    Interaction attributes

    Map your settings to QTI interaction attributes:

    question-type.json (partial)
    {
      "interchange": {
        "qti3": {
          "interactionType": "choiceInteraction",
          "export": {
            "attributes": {
              "shuffle": "$question.settings.shuffleOptions",
              "max-choices": "$question.settings.maxSelections"
            },
            "prompt": "$question.description"
          }
        }
      }
    }

    This generates:

    <qti-choice-interaction
      response-identifier="RESPONSE"
      shuffle="true"
      max-choices="1">
      <qti-prompt>Select the correct answer.</qti-prompt>
      ...
    </qti-choice-interaction>

    Choices

    For interactions with choice elements, use the choices configuration:

    question-type.json (partial)
    {
      "export": {
        "choices": {
          "source": "$question.settings.options",
          "identifier": "'choice_' & $string($index)",
          "content": "text",
          "fixed": "fixed"
        }
      }
    }
    FieldDescription
    sourceJSONata expression returning the array to map
    identifierJSONata expression for each choice's ID (has access to $index)
    contentJSONata expression for the choice text (evaluated per item)
    fixedOptional: JSONata expression for whether the choice is fixed (not shuffled)
    Tip

    Inside identifier, content, and fixed expressions, you're iterating over each item in the source array. Properties of the current item are directly accessible (e.g., text, fixed), and $index gives you the current position.

    This generates:

    <qti-simple-choice identifier="choice_0">London</qti-simple-choice>
    <qti-simple-choice identifier="choice_1">Paris</qti-simple-choice>
    <qti-simple-choice identifier="choice_2">Berlin</qti-simple-choice>

    Response declaration

    Configure the response declaration to define cardinality, base type, and correct responses:

    question-type.json (partial)
    {
      "export": {
        "responseDeclaration": {
          "identifier": "RESPONSE",
          "cardinality": "$question.settings.maxSelections = 1 ? 'single' : 'multiple'",
          "baseType": "'identifier'",
          "correctResponse": "$map($question.settings.options, function($opt, $idx) { $opt.correct ? 'choice_' & $string($idx) : null })[$ != null]"
        }
      }
    }

    The correctResponse expression filters the options array to find correct answers, then maps them to choice identifiers.

    Response processing

    Specify a standard response processing template:

    question-type.json (partial)
    {
      "export": {
        "responseProcessingTemplate": "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct.xml"
      }
    }

    Common templates:

    TemplateUse case
    match_correct.xmlExact match scoring (multiple choice, ordering)
    map_response.xmlPartial credit with score mapping
    map_response_point.xmlPartial credit for point-based responses

    Import configuration

    The import configuration defines how to extract question settings from a QTI interaction.

    Basic settings extraction

    Map QTI attributes and elements to your settings:

    question-type.json (partial)
    {
      "interchange": {
        "qti3": {
          "interactionType": "choiceInteraction",
          "import": {
            "description": "$interaction.prompt",
            "settings": {
              "shuffleOptions": "$boolean($interaction.attributes.shuffle)",
              "maxSelections": "$number($interaction.attributes.`max-choices`)"
            }
          }
        }
      }
    }
    Attribute names with hyphens

    QTI attribute names often contain hyphens (e.g., max-choices). In JSONata, wrap these in backticks:

    $interaction.attributes.`max-choices`

    Array extraction

    For extracting choice arrays, use the object syntax with source and each:

    question-type.json (partial)
    {
      "import": {
        "settings": {
          "options": {
            "source": "$interaction.choices",
            "each": {
              "text": "content",
              "correct": "identifier in $interaction.correctResponse"
            }
          }
        }
      }
    }
    FieldDescription
    sourceJSONata expression returning the source array
    eachMap of property names to JSONata expressions (evaluated per item)

    Inside each expressions:

    • Properties of the current item are directly accessible (content, identifier)
    • Use $interaction to access the full interaction context
    • Use $index for the current index

    Complete examples

    Multiple choice

    question-type.json
    {
      "id": "examplary.default.multiple-choice",
      "name": { "en": "Multiple choice" },
     
      "settings": [
        { "id": "options", "type": "array" },
        { "id": "shuffleOptions", "type": "boolean", "default": true },
        { "id": "maxSelections", "type": "number", "default": 1 }
      ],
     
      "interchange": {
        "qti3": {
          "interactionType": "choiceInteraction",
     
          "export": {
            "attributes": {
              "shuffle": "$question.settings.shuffleOptions",
              "max-choices": "$question.settings.maxSelections"
            },
            "prompt": "$question.description",
            "choices": {
              "source": "$question.settings.options",
              "identifier": "'choice_' & $string($index)",
              "content": "text"
            },
            "responseDeclaration": {
              "cardinality": "$question.settings.maxSelections = 1 ? 'single' : 'multiple'",
              "baseType": "'identifier'",
              "correctResponse": "$map($question.settings.options, function($o, $i) { $o.correct ? 'choice_' & $string($i) : null })[$ != null]"
            },
            "responseProcessingTemplate": "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct.xml"
          },
     
          "import": {
            "description": "$interaction.prompt",
            "settings": {
              "options": {
                "source": "$interaction.choices",
                "each": {
                  "text": "content",
                  "correct": "identifier in $interaction.correctResponse"
                }
              },
              "shuffleOptions": "$boolean($interaction.attributes.shuffle)",
              "maxSelections": "$number($interaction.attributes.`max-choices`)"
            }
          }
        }
      }
    }

    Essay / Extended text

    question-type.json
    {
      "id": "examplary.default.essay",
      "name": { "en": "Essay" },
     
      "settings": [
        { "id": "maxWords", "type": "number", "default": 500 },
        {
          "id": "format",
          "type": "enum",
          "options": [
            { "value": "plain", "label": "Plain text" },
            { "value": "rich", "label": "Rich text" }
          ],
          "default": "plain"
        }
      ],
     
      "interchange": {
        "qti3": {
          "interactionType": "extendedTextInteraction",
     
          "export": {
            "attributes": {
              "expected-length": "$question.settings.maxWords * 6",
              "format": "$question.settings.format = 'rich' ? 'xhtml' : 'plain'"
            },
            "prompt": "$question.description",
            "responseDeclaration": {
              "cardinality": "'single'",
              "baseType": "'string'"
            }
          },
     
          "import": {
            "description": "$interaction.prompt",
            "settings": {
              "maxWords": "$round($number($interaction.attributes.`expected-length`) / 6)",
              "format": "$interaction.attributes.format = 'xhtml' ? 'rich' : 'plain'"
            }
          }
        }
      }
    }

    Ordering

    question-type.json
    {
      "id": "examplary.default.ordering",
      "name": { "en": "Ordering" },
     
      "settings": [
        { "id": "items", "type": "array" },
        { "id": "shuffleItems", "type": "boolean", "default": true }
      ],
     
      "interchange": {
        "qti3": {
          "interactionType": "orderInteraction",
     
          "export": {
            "attributes": {
              "shuffle": "$question.settings.shuffleItems"
            },
            "prompt": "$question.description",
            "choices": {
              "source": "$question.settings.items",
              "identifier": "'item_' & $string($index)",
              "content": "text"
            },
            "responseDeclaration": {
              "cardinality": "'ordered'",
              "baseType": "'identifier'",
              "correctResponse": "$sort($question.settings.items, function($a, $b) { $a.correctPosition - $b.correctPosition }).$map($question.settings.items, function($item, $idx) { $item = $ ? 'item_' & $string($idx) : null })[$ != null]"
            },
            "responseProcessingTemplate": "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct.xml"
          },
     
          "import": {
            "description": "$interaction.prompt",
            "settings": {
              "items": {
                "source": "$interaction.choices",
                "each": {
                  "text": "content",
                  "correctPosition": "$indexof($interaction.correctResponse, identifier)"
                }
              },
              "shuffleItems": "$boolean($interaction.attributes.shuffle)"
            }
          }
        }
      }
    }

    Supported interaction types

    The following QTI 3.0 interaction types are supported:

    Interaction typeDescriptionTypical use
    choiceInteractionSingle or multiple choiceMultiple choice questions
    extendedTextInteractionLong text responseEssays, open questions
    textEntryInteractionShort text responseFill-in-the-blank, short answer
    orderInteractionArrange items in orderSequencing, ranking
    matchInteractionMatch pairs of itemsMatching exercises
    inlineChoiceInteractionDropdown within textCloze tests
    hotspotInteractionClick on image regionsImage-based questions
    gapMatchInteractionDrag items into gapsDrag-and-drop fill-in
    sliderInteractionNumeric sliderNumeric estimation
    uploadInteractionFile uploadDocument submissions
    drawingInteractionFreeform drawingDiagrams, sketches
    mediaInteractionAudio/video responseRecording responses

    Testing your configuration

    To test your interchange configuration:

    1. Create a test exam with your custom question type
    2. Export the exam to QTI format from the Examplary UI
    3. Verify the generated XML matches your expectations
    4. Import the exported QTI file into a new exam
    5. Verify the questions were reconstructed correctly
    Tip

    Use the JSONata Exerciser to debug your expressions. Set up your context with $question or $interaction as a bound variable and test each expression individually.

    Troubleshooting

    Expression returns undefined

    Make sure the path exists in your data. Use the conditional operator to provide defaults:

    $question.settings.shuffleOptions ? $question.settings.shuffleOptions : true;

    Or use the null coalescing pattern:

    $question.settings.shuffleOptions != null
      ? $question.settings.shuffleOptions
      : true;

    Attribute names with special characters

    Wrap attribute names containing hyphens or other special characters in backticks:

    $interaction.attributes.`max-choices`
    $interaction.attributes.`response-identifier`

    Array indexing issues

    Remember that JSONata arrays are zero-indexed. Use $index within array mapping contexts:

    "choice_" & $string($index); // → "choice_0", "choice_1", ...

    Accessing the interaction context inside each expressions

    Use $interaction to access the full interaction when inside an each block:

    // Inside each expression for options
    identifier in $interaction.correctResponse;

    Literal strings in expressions

    When you want a literal string value (not a property path), wrap it in quotes:

    "'identifier'"; // Returns the string "identifier"
    "'single'"; // Returns the string "single"

    Learn more