Meta-Schema
The JSON Schema a dialect composes to validate its documents.
A dialect's meta-schema is a JSON Schema (draft 2020-12) validating the dialect's documents. It composes two parts: the core definitions, fixed by this specification, and the dialect's element definitions. Any draft 2020-12 validator can then validate documents; no other tooling is required.
The core definitions alone are deliberately partial: they encode the language's constructs, while the element definitions supply everything the dialect accepts. A meta-schema composed with no element definitions is still well-formed; it validates document structure and rejects every element type.
Root
The root schema validates the document shape and delegates to the element definitions:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/ui-schema/app",
"type": "object",
"properties": {
"$id": { "type": "string", "format": "uri" },
"$schema": { "const": "https://example.com/ui-schema/app" },
},
"required": ["$id", "$schema", "type"],
"unevaluatedProperties": false,
"$ref": "#/$defs/elements",
"$defs": {/* Insert definitions here */},
}The dialect's URI appears as $id and as the $schema constant.
Everything else is fixed.
Strictness comes from unevaluatedProperties. The
element definitions behind the $ref are one
conditional per element type, and the one matching the document's type
evaluates both type and props; an unknown element type matches none,
leaving those keys unevaluated and the document rejected.
Definitions
Every meta-schema embeds these fixed definitions unchanged, completed by the dialect's element definitions:
{
"dataRef": {
"type": "object",
"properties": {
"$data": { "type": "string", "format": "json-pointer" },
},
"required": ["$data"],
"additionalProperties": false,
},
"argumentRef": {
"type": "object",
"properties": {
"$arg": { "type": "string", "pattern": "^\\$" },
},
"required": ["$arg"],
"additionalProperties": false,
},
"translationRef": {
"type": "object",
"properties": {
"$t": { "type": "string" },
"$params": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/expression" },
},
"$tags": {
"type": "object",
"additionalProperties": { "$ref": "#element" },
},
},
"required": ["$t"],
"additionalProperties": false,
},
"messageRef": {
"type": "object",
"properties": {
"$message": { "type": "string" },
"$params": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/expression" },
},
},
"required": ["$message"],
"additionalProperties": false,
},
"expression": {
"dependentSchemas": {
"$arg": { "$ref": "#/$defs/argumentRef" },
"$data": { "$ref": "#/$defs/dataRef" },
"$message": { "$ref": "#/$defs/messageRef" },
"$t": { "$ref": "#/$defs/translationRef" },
},
},
"reference": {
"anyOf": [
{ "$ref": "#/$defs/argumentRef" },
{ "$ref": "#/$defs/dataRef" },
{ "$ref": "#/$defs/messageRef" },
{ "$ref": "#/$defs/translationRef" },
],
},
"field": {
"type": "object",
"properties": {
"$field": { "type": "string", "format": "json-pointer" },
},
"required": ["$field"],
"additionalProperties": false,
},
"action": {
"type": "object",
"properties": {
"$action": { "type": "string" },
"$args": {
"type": "array",
"items": { "$ref": "#/$defs/expression" },
},
},
"required": ["$action"],
"additionalProperties": false,
},
"actions": {
"anyOf": [
{ "$ref": "#/$defs/action" },
{ "type": "array", "items": { "$ref": "#/$defs/action" }, "minItems": 1 },
],
},
"children": {
"anyOf": [
{ "$ref": "#element" },
{ "type": "string" },
{ "$ref": "#/$defs/reference" },
{
"type": "array",
"items": {
"$anchor": "child",
"anyOf": [
{ "$ref": "#element" },
{ "type": "string" },
{ "$ref": "#/$defs/reference" },
{
"$ref": "#/$defs/ifThenElse",
"properties": {
"$else": { "$ref": "#child" },
"$then": { "$ref": "#child" },
},
},
{
"$ref": "#/$defs/switchCase",
"properties": {
"$case": { "additionalProperties": { "$ref": "#child" } },
"$default": { "$ref": "#child" },
},
},
{
"$ref": "#/$defs/map",
"properties": {
"$each": { "$ref": "#child" },
},
},
],
},
},
],
},
"ifThenElse": {
"type": "object",
"properties": {
"$else": true,
"$if": true,
"$matches": { "type": ["object", "boolean"] },
"$then": true,
},
"required": ["$if", "$then"],
"additionalProperties": false,
},
"switchCase": {
"type": "object",
"properties": {
"$case": { "type": "object" },
"$default": true,
"$switch": true,
},
"required": ["$case", "$switch"],
"additionalProperties": false,
},
"map": {
"type": "object",
"properties": {
"$map": {
"anyOf": [
{ "type": "array" },
{ "$ref": "#/$defs/argumentRef" },
{ "$ref": "#/$defs/dataRef" },
],
},
"$each": true,
},
"required": ["$map", "$each"],
"additionalProperties": false,
},
"element": {
"$anchor": "element",
"type": "object",
"required": ["type"],
"unevaluatedProperties": false,
"$ref": "#/$defs/elements",
},
"elements": {/* Insert element definitions here */},
}Each definition encodes one construct:
dataRef,argumentRef,translationRef,messageRef: the four reference forms.reference: exactly one of the reference forms, paired with a position's own schema.expression: any value, with a reserved key ($arg,$data,$t,$message) held to that reference's shape; used where any literal is legal, in action arguments and message params.field: a$fieldtwo-way binding, for bindable props.action,actions: one dispatch, and one or a sequence of them, for callback props.children: nested elements, as one entry or an array of entries.ifThenElse,switchCase,map: the branch-free wrapper shapes each prop position overlays with its own branches.element: anchors nested element positions, applyingelementsto them through its$ref.
The #element and #child anchors recurse into the
element definitions.
Element Definitions
One dialect-specific definition completes the meta-schema. The value
of elements is one if/then conditional per element type,
applying that type's prop positions:
{
"allOf": [
{
"if": {
"properties": { "type": { "const": "button" } },
"required": ["type"],
},
"then": {
"properties": {
"type": { "const": "button" },
"props": {
"type": "object",
"properties": {
"label": {/* Insert prop position here */},
"onPress": {/* Insert prop position here */},
},
"required": ["label"],
"additionalProperties": false,
},
},
"required": ["type", "props"],
},
},
],
}An element requires props only when it has required props.
Prop Positions
Every declared prop is wrapped into a prop position: an anchored
anyOf pairing the prop's accepted values with the control-flow
wrappers, whose branches recurse to the position through its anchor. The
anchor is the type and prop name joined with a dot, and anchors MUST be
unique in the artifact: a dotted type name can collide with another
type's prop path (card.footer + note against card + footer.note),
and a compiler MUST reject the collision. For a label prop on a
button element:
{
"$anchor": "button.label",
"anyOf": [
/* Insert accepted values here */
{
"$ref": "#/$defs/ifThenElse",
"properties": {
"$else": { "$ref": "#button.label" },
"$then": { "$ref": "#button.label" },
},
},
{
"$ref": "#/$defs/switchCase",
"properties": {
"$case": { "additionalProperties": { "$ref": "#button.label" } },
"$default": { "$ref": "#button.label" },
},
},
],
}The accepted values depend on the prop's declaration:
- A value prop accepts its declared schema and a reference: for this
label,{ "type": "string" }and{ "$ref": "#/$defs/reference" }. - A bindable field accepts its value schema, a reference, and the
binding:
{ "$ref": "#/$defs/field" }. - A children prop accepts
{ "$ref": "#/$defs/children" }alone. - A callback prop accepts
{ "$ref": "#/$defs/actions" }alone.
Positions whose accepted value schema includes an array append the map
wrapper; an action sequence is not a value array, so callback positions
never take one. Whether a value schema counts is decided structurally,
without resolving $refs or merging compositions: a top-level items or
prefixItems, a type including "array", an array literal in const
or enum, or any allOf/anyOf/oneOf/then/else branch matching
the same probe. A children prop types $each as one child; a prop whose
probe finds exactly one items schema types it as one wrapped item; and
every other accepting shape (a tuple, an untyped array, several array
branches) leaves $each open.
A value schema also encodes the language's
deep expressions. An
expression is a literal or a reference, and the literal half is each
position's own schema, so the encoding only adds reference: every
value position nested in the schema, each object property and array
item, becomes an anyOf of the two. A badge prop declared as an
object with a string text accepts:
{
"type": "object",
"properties": {
"text": { "anyOf": [{ "type": "string" }, { "$ref": "#/$defs/reference" }] }
},
"required": ["text"]
}Complete Example
The parts assemble into this meta-schema for a dialect defining one
button element with a required label and an onPress callback:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/ui-schema/app",
"type": "object",
"properties": {
"$id": { "type": "string", "format": "uri" },
"$schema": { "const": "https://example.com/ui-schema/app" }
},
"required": ["$id", "$schema", "type"],
"unevaluatedProperties": false,
"$ref": "#/$defs/elements",
"$defs": {
"action": {
"type": "object",
"properties": {
"$action": { "type": "string" },
"$args": { "type": "array", "items": { "$ref": "#/$defs/expression" } }
},
"required": ["$action"],
"additionalProperties": false
},
"actions": {
"anyOf": [
{ "$ref": "#/$defs/action" },
{
"type": "array",
"items": { "$ref": "#/$defs/action" },
"minItems": 1
}
]
},
"children": {
"anyOf": [
{ "$ref": "#element" },
{ "type": "string" },
{ "$ref": "#/$defs/reference" },
{
"type": "array",
"items": {
"$anchor": "child",
"anyOf": [
{ "$ref": "#element" },
{ "type": "string" },
{ "$ref": "#/$defs/reference" },
{
"$ref": "#/$defs/ifThenElse",
"properties": {
"$else": { "$ref": "#child" },
"$then": { "$ref": "#child" }
}
},
{
"$ref": "#/$defs/switchCase",
"properties": {
"$case": { "additionalProperties": { "$ref": "#child" } },
"$default": { "$ref": "#child" }
}
},
{
"$ref": "#/$defs/map",
"properties": { "$each": { "$ref": "#child" } }
}
]
}
}
]
},
"ifThenElse": {
"type": "object",
"properties": {
"$else": true,
"$if": true,
"$matches": { "type": ["object", "boolean"] },
"$then": true
},
"required": ["$if", "$then"],
"additionalProperties": false
},
"switchCase": {
"type": "object",
"properties": { "$case": { "type": "object" }, "$default": true, "$switch": true },
"required": ["$case", "$switch"],
"additionalProperties": false
},
"field": {
"type": "object",
"properties": { "$field": { "type": "string", "format": "json-pointer" } },
"required": ["$field"],
"additionalProperties": false
},
"map": {
"type": "object",
"properties": {
"$map": {
"anyOf": [
{ "type": "array" },
{ "$ref": "#/$defs/argumentRef" },
{ "$ref": "#/$defs/dataRef" }
]
},
"$each": true
},
"required": ["$map", "$each"],
"additionalProperties": false
},
"argumentRef": {
"type": "object",
"properties": { "$arg": { "type": "string", "pattern": "^\\$" } },
"required": ["$arg"],
"additionalProperties": false
},
"dataRef": {
"type": "object",
"properties": { "$data": { "type": "string", "format": "json-pointer" } },
"required": ["$data"],
"additionalProperties": false
},
"expression": {
"dependentSchemas": {
"$arg": { "$ref": "#/$defs/argumentRef" },
"$data": { "$ref": "#/$defs/dataRef" },
"$message": { "$ref": "#/$defs/messageRef" },
"$t": { "$ref": "#/$defs/translationRef" }
}
},
"messageRef": {
"type": "object",
"properties": {
"$message": { "type": "string" },
"$params": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/expression" }
}
},
"required": ["$message"],
"additionalProperties": false
},
"reference": {
"anyOf": [
{ "$ref": "#/$defs/argumentRef" },
{ "$ref": "#/$defs/dataRef" },
{ "$ref": "#/$defs/messageRef" },
{ "$ref": "#/$defs/translationRef" }
]
},
"translationRef": {
"type": "object",
"properties": {
"$params": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/expression" }
},
"$t": { "type": "string" },
"$tags": { "type": "object", "additionalProperties": { "$ref": "#element" } }
},
"required": ["$t"],
"additionalProperties": false
},
"elements": {
"allOf": [
{
"if": {
"properties": { "type": { "const": "button" } },
"required": ["type"]
},
"then": {
"properties": {
"type": { "const": "button" },
"props": {
"type": "object",
"properties": {
"label": {
"$anchor": "button.label",
"anyOf": [
{ "type": "string" },
{ "$ref": "#/$defs/reference" },
{
"$ref": "#/$defs/ifThenElse",
"properties": {
"$else": { "$ref": "#button.label" },
"$then": { "$ref": "#button.label" }
}
},
{
"$ref": "#/$defs/switchCase",
"properties": {
"$case": {
"additionalProperties": { "$ref": "#button.label" }
},
"$default": { "$ref": "#button.label" }
}
}
]
},
"onPress": {
"$anchor": "button.onPress",
"anyOf": [
{ "$ref": "#/$defs/actions" },
{
"$ref": "#/$defs/ifThenElse",
"properties": {
"$else": { "$ref": "#button.onPress" },
"$then": { "$ref": "#button.onPress" }
}
},
{
"$ref": "#/$defs/switchCase",
"properties": {
"$case": {
"additionalProperties": { "$ref": "#button.onPress" }
},
"$default": { "$ref": "#button.onPress" }
}
}
]
}
},
"required": ["label"],
"additionalProperties": false
}
},
"required": ["type", "props"]
}
}
]
},
"element": {
"$anchor": "element",
"type": "object",
"required": ["type"],
"unevaluatedProperties": false,
"$ref": "#/$defs/elements"
}
}
}Versioning
The core definitions and composition rules are fixed by this
specification: revising the language revises them. A dialect's
meta-schema is composed from one revision of the language, so a
document's $schema transitively pins both the dialect and the language
semantics it was written against.