Skip to content

Itembody-only QTI Subformat

QTI-Editor stores authoring state in the QTI itembody itself, so the itembody is the single source of truth for an item — including state that QTI 3.0 does not define an attribute for (e.g. an authoring correct-response, per-interaction score, per-choice scoring).

This document explains the subformat: what extra attributes the editor reads, how the final export stays valid QTI 3.0, and how to declare a new one.

When to use this

Use this document when you need to:

  • understand the editor’s save/load format for a single item body
  • add a new interaction attribute that must survive roundtrip export/import
  • debug why a canonical attribute is stripped from the final QTI 3.0 output

The contract

  1. The roundtrip item-body (the editor’s own save/load format) carries non-QTI authoring attributes directly on the interaction as canonical, unprefixed names (e.g. correct-response, score, case-sensitive, area-mappings). There are no data-* mirrors and no marker attributes.
  2. On final QTI 3.0 export, the composer reads those canonical attributes, folds them into standard qti-response-declaration / qti-response-processing, and strips them from the emitted interaction. The exported item is therefore standard QTI 3.0 with no editor-specific attributes.
  3. On import, every QTI 3.0 item is treated as third-party: roundtripQtiItem hoists correct-response / score from the native declarations back onto each interaction as canonical attributes, restoring the authoring state.

Where non-QTI attributes are declared

Each interaction declares its non-QTI attributes in its composer metadata:

packages/prose-qti/src/components/<name>/composer/metadata.ts

export const choiceInteractionComposerMetadata = {
// ...
nonQtiAttributes: [
'score',
// Object form: alias support for legacy source-name casings
{ source: 'correct-response', aliases: ['correctResponse', 'correctAnswer'] },
],
// ...
} satisfies InteractionComposerMetadata;

Entries can be:

  • A plain string (e.g. 'score') — the attribute is read from the source element and stripped on final export.
  • An object with { source, mirror: false } — strip-only (the mirror field is retained for backwards compatibility but no longer affects output, since there are no data-* mirrors).
  • An object with { source, aliases: [...] } — additional legacy attribute names that should also be accepted on the source element and normalized to source.
// Strip-only — preserved elsewhere (e.g. as a synthesized QTI element)
{ source: 'rubricScoringBlock', mirror: false }
// With legacy alias defensive net
{ source: 'correct-response', aliases: ['correctResponse', 'correctAnswer'] }

The compose handler reads these from the source element to populate the response declaration, then removes them from the normalized element so the final export is clean standard QTI 3.0.

Current non-QTI attribute set

AttributeInteractionsRecovered on import from
correct-responsechoice, extended-text, associate, hottext, text-entry, match, gap-match, order, inline-choice, select-pointqti-correct-response values of the matching qti-response-declaration
scoreall interactions aboveqti-response-processing (defaults to 1)
case-sensitivetext-entryeditor authoring hint — not reconstructed from standard QTI
area-mappingsselect-pointeditor authoring hint — not reconstructed from standard QTI

case-sensitive and area-mappings have no lossless standard-QTI source, so a third-party item that never carried them imports without them.

How recovery works on import

Every QTI 3.0 item is treated as third-party. roundtripQtiItem (@citolab/prose-qti/qti3-item-import) runs a chain of idempotent transforms that hoist correct-response / score from native qti-response-declaration / qti-response-processing onto each interaction as canonical attributes. Per-type transforms run first (they carry type-specific behaviour such as the extended-text rubric source); a generic fallback then covers any remaining interaction with a response-identifier. A final roundtripItemBody transform copies identifier / title from the qti-assessment-item onto the qti-item-body.

Adding a new non-QTI attribute

To extend the subformat with a new attribute on an interaction:

  1. Add the attribute to nonQtiAttributes in the interaction’s composer/metadata.ts. Use a plain string for the simple case; the object form { source, mirror: false } for strip-only attributes (preserved elsewhere); the object form { source, aliases: [...] } to also accept legacy source-name casings on input.
  2. Read the attribute in the interaction’s .compose.ts from the source element and use it for whatever the interaction needs (response declaration, UI state, additional element synthesis like extended-text’s rubric block).
  3. If the attribute must survive a third-party round-trip, ensure roundtripQtiItem can reconstruct it from standard QTI; otherwise document it as an editor-only hint.