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/ 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):
{
"$schema": "../../.engine/schemas/com.hikari.material.v1.schema.json",
"kind": "com.hikari.material",
"version": 1
}kindandversionare required.versionis a positive integer matching the registry’s current version. Older and newer versions are refused.$schemais 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.jsonsidecars 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:
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; the registry is 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
- 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. - Bind it (
assets/document_schemas.zig): the Zig struct, optionalextra_defs. - Render: from
src/hikari,zig build generate-document-schemas, and commitschemas/. - If Shinra cooks it, add the
AuthoringKindvariant and its cook; theschema::testsbijection test refuses a mismatch. If the editor lists it, map it ineditor_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.