# Document schemas

Every JSON file the engine, editor, Shinra or Kaji read is a **document kind**: a file with a `kind` tag (`com.hikari.material`), an integer `version`, and a shape described by a JSON Schema (draft 2020-12) rendered from the Zig type that parses it. The rendered files live in [`src/hikari/schemas/`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/schemas/) and are staged into every project as `<project>/.engine/schemas/` by the editor when it opens the project (the folder is derived output under the ignored `.engine/`; a fresh clone has it after the first editor open). Read the schema before writing a document by hand or from an agent; the editor, the cook and the MCP tools all check against it.

## What exists

| File | Purpose |
|---|---|
| `schemas/document_kinds.json` | The registry: every kind's tag, how a file is recognised (compound suffix such as `.material.json`, a fixed project path such as `configs/render.json`, or the `kind` tag of a bare `.json`), current version, whether the envelope may be implied, the cooked companion suffix, and the schema file name. |
| `schemas/<tag>.v<N>.schema.json` | One current schema per kind. `$id` is `urn:hikari:schema:<tag>:v<N>`. Nested shapes are `$defs`; every object is `additionalProperties: false` unless the type says otherwise (game components under `components`, free-form slot maps). |

Kinds today: scene, prefab, material (with the embedded material graph), model document, particle system, animation graph, sprite atlas, UI document, localization catalog, import-settings sidecar (`.shinmeta.json`), project descriptor, plugin package and project plugins, gameplay tags, and the nine `configs/*.json` documents.

## The envelope

An envelope fragment (add the document-specific fields):

```json
{
  "$schema": "../../.engine/schemas/com.hikari.material.v1.schema.json",
  "kind": "com.hikari.material",
  "version": 1
}
```

- `kind` and `version` are required. `version` is a positive integer matching the registry’s current version. Older and newer versions are refused.
- `$schema` is a hint the editor stamps (relative path to the staged copy). Every reader ignores it.
- Two kinds allow an **implied** envelope as part of their authoring format: localization catalogs and `.shinmeta.json` sidecars read as version 1 when the envelope is absent, and the editor stamps it on save.
- The sidecar's wire shape is `assets/meta/sidecar_json.zig`, mirroring what Shinra reads. The inspector's field table is a projection of it: a comptime lock refuses a row that names no wire field and a wire leaf with no row (or waiver), and a Shinra test validates what serde writes against the schema. An inspector save overlays its rows onto the file as it is on disk, so hand-authored blocks it does not model (`material_overrides`, `spatial_clusters`) survive.

## Who checks what

| Reader | Behaviour |
|---|---|
| Editor (`EditorContent.readText`, `Config.load`, scene open) | Checks the current (or implied) envelope, validates against the schema and logs each issue as a warning with its path (`$.material.params.metalic: unknown property 'metalic' (did you mean 'metallic'?)`). A document with a typo still opens; the next save writes it canonical. |
| Editor writes (`EditorContent.writeText`, `Config.write`, project settings) | Stamp `$schema`, `kind` and the current integer `version` first, keep every other key and number token as written. A file that names another kind is written untouched. |
| Shinra cook and watch | Refuse a document whose envelope is not current (or implied) or that fails the schema, naming the path. `shinra validate <file-or-dir>…` runs the same check from the command line; fixed project files (`configs/*.json`, `hikari.project.json`) classify by where they sit, and a file named on the command line that matches no kind fails rather than being skipped. |
| Kaji | Reads `configs/layout.json`, `configs/packaging.json` and `hikari.project.json` and takes their versions from the registry. Kaji never touches `.engine/schemas/`; the editor stages it on project open. |
| MCP | `document_schema_get` lists kinds or returns one schema; `hikari://schema/documents` is the same list as a resource. `asset_document_get` / `asset_document_edit` report `schema_issues` and refuse a patch that breaks the shape. |

## Where shapes live

The Zig struct `std.json` parses *is* the schema. Reflection turns fields into properties, a field without a default into a required property, `?T` into an optional-or-null property, enums into `enum` lists and `[N]T` into fixed-length arrays. Three declarations add what reflection cannot see:

```zig
pub const Document = struct {
    kind: []const u8,
    version: u32,
    capacity: u32 = 65_536,
    emitters: []const Emitter,

    pub const schema_doc = "GPU particle system.";                       // object description
    pub const schema_additional_properties = false;                        // default; true allows extra keys
    pub const schema_meta = .{                                             // per-field constraints
        .capacity = .{ .description = "Total particles.", .minimum = 1 },
        .texture = .{ .format = "asset-ref" },
    };
};
```

A type with its own `jsonParse` provides `pub fn jsonSchema(arena) !std.json.Value`. A table-driven shape (the import-settings sidecar, the UI node vocabulary) builds its body from the same table the editor uses. Every document struct must be **named**: an anonymous struct's type name is numbered per compilation and would make the rendered file unstable.

| Kind | Zig shape |
|---|---|
| scene, prefab | `scene/scene_json_loader/json_types.zig` (`SceneJsonRoot`, `PrefabJsonRoot`) |
| material | `assets/material_asset.zig` (`MaterialJsonRoot`; node kinds from `editor/material_graph/nodes.schema.json`) |
| model document | `assets/formats/model_doc_json.zig` |
| particle system | `assets/formats/particle_json.zig` (Shinra's serde structs are the parser; a Shinra test holds the two to one field set) |
| animation graph | `assets/formats/animation_graph_json.zig` |
| sprite atlas | `assets/formats/sprite_atlas_json.zig` |
| UI document | `ui/document/document_json.zig` (node vocabulary rendered from `ui/document/spec.zig`) |
| localization | `assets/formats/localization_json.zig` |
| import sidecar | `assets/meta/schema.zig` (`Document.jsonSchema` from the field table) |
| project descriptor | `project_descriptor.zig` |
| plugin manifests | `sdk/src/plugins/manifest_json.zig` (import-free; the build-side parser is held to it by `fixtures/full_manifest.json`) |
| gameplay tags | `scene/world/world_tags.zig` (`CatalogJson`) |
| configs | `sdk/src/project_config/*.zig` and `input/input_*_json.zig` |

The binding table is [`src/hikari/src/assets/document_schemas.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/assets/document_schemas.zig); the registry is [`sdk/src/document_schema/registry.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/sdk/src/document_schema/registry.zig). Shinra embeds the rendered files through its `build.rs` and classifies with `shinra::schema`; Kaji reads them through `DocumentKinds`.

## Adding a kind

1. Add a registry row (`sdk/src/document_schema/registry.zig`): tag, suffix / fixed file / bare tag, `current = 1`, envelope policy, cooked suffix if Shinra cooks it.
2. Bind it (`assets/document_schemas.zig`): the Zig struct, optional `extra_defs`.
3. Render: from `src/hikari`, `zig build generate-document-schemas`, and commit `schemas/`.
4. If Shinra cooks it, add the `AuthoringKind` variant and its cook; the `schema::tests` bijection test refuses a mismatch. If the editor lists it, map it in `editor_content.diskKindOf`.

The tests refuse a registry row without a binding, a stale rendered file, an anonymous document struct, and a Shinra kind set that differs from the registry's cooked rows.

## Changing a shape

- **Adding an optional field with a default** keeps the version. Re-render; the stale-file test tells you when.
- **Removing, renaming or retyping a property** bumps `current`. Update authored documents and regenerate the schema in the same change. Every reader accepts only the current version (or an implied envelope where allowed); there is no migration chain or compatibility range.

## Game data

Game tables read through `hi.json` are not registry kinds, but `hi.json.schema(T, allocator, .{ .id, .title })` renders the same JSON Schema for a game's own table type, so a project can publish shapes for its `content://…json` files the same way.
