← Split overviewCustom typesPattern 4 of spec splitting, in detail. Declare reusable row / entry shapes once inside `dataFlow.customTypes` and reference them by name (`[ActivityRow]`) from any `uiVariables` or VM var. A lighter-weight DRY knob than splitting into sub-specs — the unit of reuse here is the *type shape*, not a slice of state.~9 min read
Why declare custom typesJSON isn't a typed language, but the spec is. Every `uiVariables` / `vars` entry has a `type` string, and the generator treats that string as a contract — it will fail to emit a hand-written VM that breaks it. For primitives (`String`, `Int`) that's already enough; for row / entry shapes you'd otherwise inline an anonymous record literal everywhere they appear. Promoting them to `dataFlow.customTypes` gives the shape a *name*, a single declaration site, and referencability from the VM contract, userActions, and (with one more step) other specs.
Declaring a typeEach entry in `dataFlow.customTypes[]` has a `name` and a `properties` array of `{ name, type }`. The same `type` vocabulary that `uiVariables` uses is available here — primitives, optionals (`T?`), arrays (`[T]`), closures (`(A) -> B`), and references to other customTypes by name. After a type is declared you can use `[ActivityRow]` / `ActivityRow?` / `Map(String, ActivityRow)` anywhere a `type:` string is expected.
dataFlow.customTypes
// dataFlow.customTypes — declare once at the top of the spec, then// reference by name from anywhere in uiVariables / VM vars / other types.{ "dataFlow": { "customTypes": [ { "name": "ActivityRow", "properties": [ { "name": "id", "type": "String" }, { "name": "title", "type": "String" }, { "name": "timestamp", "type": "Date" }, { "name": "url", "type": "String?" }, // optional { "name": "onTap", "type": "() -> Void" } // event callback ] } ] }, "stateManagement": { "uiVariables": [ { "name": "rows", "type": "[ActivityRow]", "initial": "[]" } ] }}The type vocabularyThe cheat sheet below lists the canonical forms the validator / generator recognize. Anything not on this list has to be wrapped in a customType or registered in `.jsonui-type-map.json`. Nesting works as expected — `[Map(String, ActivityRow)]?` is legal and means 'optional array of string-keyed ActivityRow maps'.
type syntax cheatsheet
Type syntax supported in `type:` fields-------String primitiveInt / Double / Bool primitives (Double is the floating-point form)Date ISO-8601 timestampVoid the no-value type (return of fire-and-forget closures)Data opaque binary payloadURL URL / Uri (string under the hood, typed on the runtime) [T] array of TMap(K, V) dictionary / hashT? optional T (allowed to be null / nil / undefined) (A, B) -> Void closure taking A + B, returning nothing (event callback)(A) -> B closure taking A, returning B AsyncThrowingStream<T, E> typed async iterator (iOS-flavoured; platform-mapped) <CustomName> reference to a customTypes entry by name[<CustomName>] array of that custom type[<CustomName>]? optional array of that custom type Most combinations nest as expected: `[Map(String, ActivityRow)]?`, etc.Sharing types across specsBy default a customType is visible only inside the spec that declares it. To share it with other specs, register it in `.jsonui-type-map.json` at the repo root — each entry records the declaring spec, the class name the generator should use, and per-platform import hints. Once registered, any other spec can reference the type by name without redeclaring the properties, and the generator wires up the correct import on each platform.
.jsonui-type-map.json
// .jsonui-type-map.json (at the repo root) — cross-spec type registry.// Register any customType you want to share; once registered, another// spec can reference it by name without redeclaring.{ "version": "1.0", "types": { "ActivityRow": { "declaredIn": "docs/screens/json/learn/first-screen.spec.json", "class": "ActivityRow", "imports": { "swift": ["CommonDomain"], // Swift Package / framework "kotlin": ["com.example.app.domain"], // Kotlin package "web": { "from": "@/models/ActivityRow" } // TS import path } } }} // Spec B (/learn/archive.spec.json) — references ActivityRow by name.// No customTypes entry needed; the registry resolves it.{ "stateManagement": { "uiVariables": [ { "name": "archived", "type": "[ActivityRow]", "initial": "[]" } ] }}What the generator emitsEach customType becomes a platform-native shape at build time. On iOS the generator emits a `struct ActivityRow { … }`. On Android it becomes a `data class ActivityRow(...)`. On web it's a TypeScript `interface ActivityRow { … }` plus a `createActivityRow()` factory for default values. The emitted files land in each platform's generated data directory (`jsonui-doc-web/src/generated/data/ActivityRowData.ts` on web). If the type is registered in `.jsonui-type-map.json` with an external `import`, the generator emits an import of the external class instead of declaring its own.
Live examples on this siteMost screens on this site use at least one customType. `NextReadLink` — the 4-field row shape that backs every 'Keep going' section at the bottom of an article — is declared in a dozen spec files. `ActivityRow` is the row model in `/learn/first-screen`. `QuickstartStep` is the per-step shape in `/learn/hello-world` with six fields and two optionals. All three stay spec-local (not in the type map) because each is only used inside its owning spec; the patterns below show you when to promote one to the repo-wide type map.
When NOT to use customTypesTwo anti-patterns. (1) Declaring a customType for a shape used in exactly one spot — the anonymous record keeps reading better. The break-even is 3+ reuse sites or a shape with 4+ properties. (2) Using the type map to share state across screens — that's the Pattern 2 (parent + sub) problem wearing a different hat. Custom types share *shape*; state slices are different instances and belong in their own sub-specs or repositories.
Keep goingFour patterns down, one to go — cellClasses.
Component specsPattern 3 — extract reusable custom UI into its own component_spec.json./spec/component-spec
Collection cell classesPattern 5 — hoist each Collection cell layout into its own file under docs/screens/layouts/cells/./spec/cell-classes