> ## Documentation Index
> Fetch the complete documentation index at: https://docs.asakiri.com/llms.txt
> Use this file to discover all available pages before exploring further.

# File formats

> The JSON for collections, records, assets, lessons, parts, compositions, and exercises.

<Info>
  Canonical format, version 1. Every example below is copied from the `japanese-starter`
  fixture, which is validated against the published
  [JSON Schemas](https://github.com/AsakiriLingo/asakiri-studio/tree/main/schemas/asakiri-course/v1)
  on every build.
</Info>

## The envelope

**Every** file in a course starts with the same two keys, not just the manifest:

```json theme={null}
{
  "format": "asakiri-course",
  "formatVersion": 1
}
```

They are omitted from the examples below for brevity. A reader should check them on each
file it opens, because a file copied between courses carries its own version.

## Versioning and migration

`formatVersion` is a monotonic integer, raised only for a breaking change.

| You meet | Do |
| - | - |
| A version you support | Read it. |
| A **lower** version | Upgrade it in memory by applying each migration step above that version in order. |
| A **higher** version | Refuse the file with a clear message naming both versions. Do not guess. |
| No envelope at all | Treat it as version 0, from before the format was stamped, and migrate it forward. |

Studio upgrades in memory on read and only writes the upgraded shape when that file is
next saved, so opening a course never rewrites it.

These changes do **not** raise the version, and readers must tolerate them:

* a new optional key on an existing file type;
* a new exercise type, composition block type, or part content kind.

## Unknown content

A reader that meets a `content.kind`, exercise `type`, or block `type` it does not know
must **keep the file verbatim** rather than fail the course. Studio parses such a part as:

```ts theme={null}
{ kind: "unknown", declaredKind: string, declaredType: string | null, raw: unknown }
```

The lesson still opens, the part keeps its position in the order, the editor shows a
non-editable placeholder, and the body file is left untouched on save. That is what lets
an older build open a course authored by a newer one without destroying data.

A **known** type carrying a malformed payload is still an error. Tolerance covers new
vocabulary, not corruption.

## Localized text

Any authored string may be a plain string in the course's `defaultLocale`, or a map of
locale tag to string:

```json theme={null}
"title": "Practice 猫"

"title": { "en": "Practice 猫", "ja": "猫の練習" }
```

This applies to the project title, subtitle, and description; unit titles; lesson, part,
and part-content titles; exercise instructions; and the `text` of a literal text fragment.
Taught content is not localized this way: it lives in records, where a language is a field
and list items carry their own `locale`.

Resolve with an exact tag match first, then the base language (`pt-BR` to `pt`), then any
available entry. A writer that edits one locale must preserve the others.

## Contributors

A contributor may hold several roles. `roles` is the list; `role` repeats the first entry
so readers written against version 1 keep working.

```json theme={null}
"contributors": [
  { "id": "c1", "name": "Alok", "role": "author", "roles": ["author", "voice"], "links": [] }
]
```

The course's own `version` and `releasedOn` (as `YYYY-MM-DD`) also live on `project`. Both
are authored metadata and have nothing to do with `formatVersion`, which describes the file
schema.

## Collection

A collection defines fields and lists its record files (paths relative to the collection file).

```json theme={null}
{
  "id": "collection_vocabulary",
  "name": "Vocabulary",
  "description": "Reusable vocabulary records shown as a table in the Content area.",
  "fields": [
    { "id": "field_japanese", "name": "Japanese", "kind": "text", "cardinality": "one", "locale": "ja", "required": true },
    { "id": "field_english", "name": "English", "kind": "text", "cardinality": "one", "locale": "en", "required": true },
    { "id": "field_pronunciations", "name": "Pronunciations", "kind": "asset", "assetKind": "audio", "cardinality": "many", "required": false },
    { "id": "field_alternate_answers", "name": "Alternate answers", "kind": "text", "cardinality": "many", "locale": "en", "required": false }
  ],
  "recordFiles": ["../records/cat.json", "../records/dog.json", "../records/bird.json"]
}
```

See [Content records](/data-structure/content-records) for the field type reference.

## Record

One record per file. A `many` field stores a `list`; each list item carries a stable `id`.

```json theme={null}
{
  "id": "record_cat",
  "collectionId": "collection_vocabulary",
  "fields": {
    "field_japanese": { "kind": "text", "value": "猫" },
    "field_english": { "kind": "text", "value": "cat" },
    "field_pronunciations": {
      "kind": "list",
      "items": [
        { "id": "pronunciation_cat_ja", "kind": "asset", "label": "Japanese (primary speaker)", "locale": "ja", "assetId": "asset_cat_ja_audio" }
      ]
    },
    "field_alternate_answers": {
      "kind": "list",
      "items": [
        { "id": "answer_cat_with_article", "kind": "text", "value": "a cat" },
        { "id": "answer_cat_plural", "kind": "text", "value": "cats" }
      ]
    }
  },
  "presentations": [
    { "id": "presentation_cat_primary", "primaryFieldId": "field_japanese", "columns": [{ "fieldId": "field_english", "visible": true }] }
  ]
}
```

## Asset

Each asset lives in its own directory with an `asset.json` and (when ready) the binary file.

```json theme={null}
{
  "id": "asset_cat_ja_audio",
  "kind": "audio",
  "label": "猫 (Japanese pronunciation)",
  "availability": "placeholder",
  "file": null,
  "expectedFile": "original.wav",
  "mimeType": "audio/wav",
  "metadata": { "locale": "ja", "speaker": "Primary speaker", "transcript": "猫" }
}
```

A ready asset also records the digest of its stored file:

```json theme={null}
{
  "id": "asset_cat_image",
  "kind": "image",
  "availability": "ready",
  "file": "original.svg",
  "mimeType": "image/svg+xml",
  "sha256": "f1f53575998a8c612b6d974dd1b6cc6d65bf8e0a55f92df6040e570009071cca",
  "byteSize": 1124
}
```

* `availability` is `ready` or `placeholder`. A placeholder has `file: null` and names the
  file it expects in `expectedFile`.
* `sha256` and `byteSize` describe the file **as stored in the project**, not the file the
  author picked. Studio strips EXIF from images on import, which changes the bytes, so the
  digest is computed after the copy. Use it for integrity checks and as a cache key.
* When ready, `file` names the binary in the same directory (for example `original.svg`).
  Resolve it relative to the asset's folder.
* Never embed binary data or absolute paths. Content references an asset by `id` only.

See [Media](/data-structure/media) for the type and supported formats.

## Lesson and parts

A lesson is an ordered list of parts. Each part points at one file and declares its kind.

```json theme={null}
{
  "id": "lesson_meet_neko",
  "title": "Meet 猫",
  "parts": [
    { "id": "part_intro", "title": "Meet your first word", "content": { "kind": "tiptap", "file": "parts/intro/document.json" } },
    { "id": "part_in_media", "title": "Cat in words, sound, and image", "content": { "kind": "composition", "file": "parts/in-media/composition.json" } },
    { "id": "part_choose", "title": "Choose the matching word", "content": { "kind": "exercise", "file": "parts/choose/exercise.json" } }
  ]
}
```

| `content.kind` | File | Format |
| - | - | - |
| `tiptap` | `document.json` | [Rich text](/build/rich-text) |
| `composition` | `composition.json` | Below |
| `exercise` | `exercise.json` | Below |

## Composition

A rich-media composition is a list of blocks. Each block has a `type`, a
[binding](/data-structure/bindings), and presentation hints. Observed block types are
`content-card`, `media`, and `callout`.

```json theme={null}
{
  "blocks": [
    {
      "id": "block_cat_card",
      "type": "content-card",
      "binding": { "kind": "record", "recordId": "record_cat" },
      "presentation": { "view": "vocabulary-card", "showFieldIds": ["field_japanese", "field_english"] }
    },
    {
      "id": "block_cat_image",
      "type": "media",
      "binding": { "kind": "item", "recordId": "record_cat", "fieldId": "field_images", "itemId": "image_cat_primary" },
      "presentation": { "fit": "contain", "caption": "猫 (cat)" }
    },
    {
      "id": "block_detached_note",
      "type": "callout",
      "binding": { "kind": "literal", "value": { "type": "text", "text": "This note is detached and will not change with the record." } },
      "presentation": { "tone": "note" }
    }
  ]
}
```

## Exercise

The on-disk exercise JSON mirrors the TypeScript interfaces in
[Exercises](/data-structure/exercises): a common envelope (`id`, `type`, `instruction`,
`prompt`, `evaluation`) plus a type-specific body. Two examples:

<CodeGroup>
  ```json multiple-choice theme={null}
  {
    "id": "exercise_identify_cat",
    "type": "multiple-choice",
    "instruction": "Choose the English word that matches the image and audio.",
    "prompt": [
      { "id": "prompt_cat_image", "role": "visual", "binding": { "kind": "item", "recordId": "record_cat", "fieldId": "field_images", "itemId": "image_cat_primary" } },
      { "id": "prompt_cat_audio", "role": "audio", "binding": { "kind": "item", "recordId": "record_cat", "fieldId": "field_pronunciations", "itemId": "pronunciation_cat_ja" } }
    ],
    "options": [
      { "id": "option_cat", "body": [ { "id": "option_cat_text", "role": "primary", "binding": { "kind": "field", "recordId": "record_cat", "fieldId": "field_english" } } ] },
      { "id": "option_dog", "body": [ { "id": "option_dog_text", "role": "primary", "binding": { "kind": "field", "recordId": "record_dog", "fieldId": "field_english" } } ] }
    ],
    "evaluation": { "kind": "selected-options", "correctOptionIds": ["option_cat"] }
  }
  ```

  ```json word-order theme={null}
  {
    "id": "exercise_cat_word_order",
    "type": "word-order",
    "instruction": "Put the tiles in order to say \"This is a cat.\"",
    "prompt": [ { "id": "p", "role": "primary", "binding": { "kind": "literal", "value": { "type": "text", "text": "Build: This is a cat." } } } ],
    "tokens": [
      { "id": "token_kore", "body": [ { "id": "t1", "role": "primary", "binding": { "kind": "literal", "value": { "type": "text", "text": "これ" } } } ] },
      { "id": "token_neko", "body": [ { "id": "t2", "role": "primary", "binding": { "kind": "field", "recordId": "record_cat", "fieldId": "field_japanese" } } ] }
    ],
    "distractors": [
      { "id": "distractor_inu", "body": [ { "id": "d1", "role": "primary", "binding": { "kind": "field", "recordId": "record_dog", "fieldId": "field_japanese" } } ] }
    ],
    "evaluation": { "kind": "ordered-tokens", "correctOrder": ["token_kore", "token_neko"] }
  }
  ```
</CodeGroup>

Correctness always refers to the stable IDs the exercise defines (`option`, `token`,
`pair`, and `blank` IDs) or to accepted-value bindings, never to displayed text. See
[Resolving and rendering](/build/resolving-and-rendering#grading) for how each `evaluation`
kind is graded.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.