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 $field two-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, applying elements to 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.

On this page