Skip to content

JSON Schema support

Texaryn’s @texaryn/schema-json adapter supports Draft 4, Draft 7, 2019-09 and 2020-12, with automatic dialect detection. Draft support identifies the schema dialect used by the adapter; it is not a claim that every keyword produces a form control or that Texaryn provides full specification compliance.

Draft $schema value Status
Draft 4 http://json-schema.org/draft-04/schema# Supported
Draft 7 http://json-schema.org/draft-07/schema# Supported
2019-09 https://json-schema.org/draft/2019-09/schema Supported
2020-12 https://json-schema.org/draft/2020-12/schema Supported
Draft 6 Not supported

The adapter reads $schema. A missing or unrecognized value falls back to Draft 7, rather than rejecting the schema. Set defaultDialect to change that fallback; it does not override a recognized $schema value.

External schema resources must use the root schema’s dialect. References to resources using a different dialect are rejected. Custom dialects and user-defined vocabularies are not a supported extension point. The adapter uses its built-in validation keywords even when a custom metaschema omits the validation vocabulary, and it does not guarantee rejection of every unknown required vocabulary. Use one of the four listed standard dialects when schema semantics must be portable.

import { createJsonSchemaAdapter } from '@texaryn/schema-json'
const adapter = await createJsonSchemaAdapter(schema, {
defaultDialect: '2020-12',
})

Use an explicit $schema value when sharing schemas so that the intended dialect is clear.

The adapter projects schemas and current data into fields, constraints and annotations for the renderer. The following describes that projection scope, separately from validation.

CapabilityDraft 4Draft 72019-092020-12Form behavior
Objects and fieldstype, properties, requiredtype, properties, requiredtype, properties, requiredtype, properties, requiredDeclared properties appear even before data is entered.
Local references$ref, definitions$ref, definitions$ref, $defs$ref, $defsReferenced field schemas are resolved locally; a recursive reference expands once past the data.
CompositionallOf, anyOf, oneOfallOf, anyOf, oneOfallOf, anyOf, oneOfallOf, anyOf, oneOfallOf contributes all branches; anyOf/oneOf selection depends on current data.
Conditionals—if, then, elseif, then, elseif, then, elseInactive branch fields remain in the projection with active: false.
DependenciesdependenciesdependenciesdependentSchemas, dependentRequireddependentSchemas, dependentRequiredPresent trigger properties activate dependent fields or required flags.
Homogeneous arraysitemsitemsitemsitemsItem fields are projected for entries present in the data.
ChoicesenumenumenumenumEnum values become options for the renderer.
String constraintsminLength, maxLength, patternminLength, maxLength, patternminLength, maxLength, patternminLength, maxLength, patternConstraints are exposed to widgets.
Numeric constraintsminimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOfminimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOfminimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOfminimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOfConstraints are exposed to widgets.
Array constraintsminItems, maxItems, uniqueItemsminItems, maxItems, uniqueItemsminItems, maxItems, uniqueItemsminItems, maxItems, uniqueItemsConstraints are exposed in the projection.
Field annotationstitle, description, default, formattitle, description, default, examples, readOnly, writeOnly, formattitle, description, default, examples, readOnly, writeOnly, format, deprecatedtitle, description, default, examples, readOnly, writeOnly, format, deprecatedAnnotations are passed to the runtime and widgets; none of them guarantees a dedicated control, and default is not applied to the data.

Projection has these limits:

  • Tuple arrays (array-valued items in older drafts or prefixItems in 2020-12) do not have a dedicated projection path. Use homogeneous arrays for generated item controls.
  • patternProperties and additionalProperties do not generate fields for arbitrary object keys. Declare editable fields in properties.
  • Boolean schemas and schemas without a resolvable type do not produce ordinary field nodes. For a type array, projection uses the first recognized type rather than creating a type selector.
  • Unresolved oneOf object branches can remain inactive. The fallback does not cover unresolved primitive alternatives; a union is not automatically a branch picker.
  • Fields declared only under a branch the specification never evaluates (a then or else without if, a then under if: false, an else under if: true) are not projected, unless a reference points into that branch or a schema inside it declares an $id, $anchor or $dynamicAnchor. Validation still runs on the schema as written, so an error that only such a branch declares attaches to no field.
  • Remote schema loading is controlled by the host supplied resource resolver. Static external $ref resources project. Dynamic scope aware projection is opt in as described below.

A local $ref can point back to a schema that contains it: "$ref": "#" for the root, or a definition under $defs or definitions that refers to itself directly or through other definitions. The form shows such a schema once past the data: below the last location that holds data, a path never applies the same schemas twice. Take this schema:

{
"type": "object",
"properties": {
"name": { "type": "string" },
"child": { "$ref": "#" }
}
}

With {} the form shows /name, /child and /child/name. Once /child holds an object, /child/child and /child/child/name appear, and each level the user fills exposes the next. { "child": null } shows the same fields as {}. An array row counts as data, even when it holds null, so the fields of every row are shown.

When the projection withholds a location, an object above it says why in NodeProjection.boundaries, which the compiler copies to ContainerNode.boundaries:

  • recursion: a descendant of this object would apply the same schemas as the object itself, and the object is past the data. That descendant is not projected and is not listed in its parent’s children.
  • budget: the projection withheld some members of this object to stay within its limits. The limits cover only locations that exist because of the recursion, two or more levels below the nearest location that holds data: one projection shows at most 16 such objects and 512 such nodes. The limits are fixed, and they never apply inside the data: every declared member of a location that holds data is projected. A withheld object is not listed in children, and the object that carries budget still shows its own fields.

Each is absent on an object for which its condition does not hold. An adapter can also expose boundaryTargets with opaque tokens for withheld locations. The built-in bindings offer expansion controls for those targets. A custom widget dispatches { type: 'ExpandBoundary', containerId, targetToken } using the container’s current target token. Expanding reveals more fields without writing data. Automatic expansion still follows the data as described above.

initialization: 'schema-defaults' never writes into recursion expanded past the data. A node the projection reached only by expanding the recursion carries NodeProjection.recursiveExpansion, and each absent one that declares a default, or whose declarations disagree, is reported as a refusal with reason recursive-expansion: the field is shown, but its default is not written, because every level written would expose another. At a node whose schemas lie on a cycle, a default that opens a level (an object or a non-empty array) is refused as recursive-default when an ancestor that holds data, or that the same run wrote, has a default from one of the same sources. That stops a recursive default from recreating itself, including across edits, while a default of [], null or a scalar is written. The limits never change the data initialization writes.

A schema that applies itself at one instance location, without crossing into a property or an item, makes the evaluator recurse without end, for example allOf: [{ $ref: '#' }] at the root. createJsonSchemaAdapter rejects it with SameLocationCycleError, exported from @texaryn/schema-json. Its positions field lists each cycle’s schema positions, and its message names one of them and the path. For allOf: [{ if: { properties: { flag: { const: true } } }, then: { $ref: '#' } }] at the root the message is:

Schema position "#" applies itself to the instance location it is evaluating, without crossing into a property or item: "#" (allOf/0) "#/allOf/0" (then) "#/allOf/0/then" ($ref) "#". The evaluator recurses without end wherever this position is reached, so the schema is rejected.

The check reads the schema, not the data, so a cycle behind a conditional is rejected even while its if does not hold. It exempts only the branches the specification never evaluates, listed above. It is conservative for dynamic references: a 2020-12 $dynamicRef to a $dynamicAnchor counts as reaching every $dynamicAnchor of that name, and a 2019-09 $recursiveRef counts as reaching every schema that declares $recursiveAnchor: true, because which one applies is decided during evaluation. A schema whose dynamic references could close such a cycle is rejected even when no evaluation would select it. A reference keyword from another dialect, such as $dynamicRef in Draft 7, is not a reference there.

The @texaryn/schema-json adapter can project active dynamic scopes when the config sets dynamicReferenceProjection: 'local':

const adapter = await createJsonSchemaAdapter(schema, {
dynamicReferenceProjection: 'local',
resolveResource: (uri) => schemaResources.get(uri),
})

Draft 2020-12 $dynamicRef and Draft 2019-09 $recursiveRef: '#' are supported below object properties and homogeneous array items. The adapter also supports these references through allOf, anyOf, oneOf, conditionals and dependentSchemas. A dynamic target may be in the input schema or in a resource returned by resolveResource. The resolver owns fetching and trust policy. Texaryn does not perform network requests.

The default projection does not apply this dynamic scope handling. The opt in mode rejects references below not, contains, patternProperties, tuple items, prefixItems, additionalProperties, unevaluatedProperties and other unsupported applicators. It also rejects root relative resource IDs, assertion siblings beside $dynamicRef, and $recursiveRef values other than '#' or with siblings. Projection support is limited to the cases listed above. ADR-010 records the supported paths.

initialData supplies values. By default, schema default annotations are metadata and do not write data into the form. Opt in through the runtime options:

const form = useForm(adapter, {
initialData: { name: 'Ada' },
initialization: 'schema-defaults',
})

Initialization fills absent values with defaults from the active projection; it does not overwrite supplied values. Conflicting defaults and defaults that would expand recursive data indefinitely are refused. The runtime’s initialization store reports writes and refusals. The recursive-reference section above describes the recursion limits.

validate(data) delegates to json-schema-library using the selected draft, then normalizes errors to JSON Pointer locations and keyword names. Validation can enforce constraints that have no corresponding form control, such as const, not, contains or restrictions on additional properties. Newer draft keywords such as minContains, maxContains, unevaluatedProperties and unevaluatedItems are delegated to that library; they are not an independent Texaryn compliance guarantee.

The production adapter and the private Hyperjump comparison adapter each pass all 618 mandatory Draft 4 cases at the pinned JSON Schema Test Suite revision. These are validation results. Draft 4 tuple-valued items does not produce tuple-specific form controls, and external resources using another dialect are not supported.

For 2019-09 and 2020-12, the adapter never asserts the schema’s own format keywords, because those dialects make format an annotation by default. A value such as format: 'email' stays available as field metadata but does not make an invalid email fail validation, and declaring the format-assertion vocabulary in $vocabulary does not turn assertion on.

Draft 4 and Draft 7 assert format, which their specifications permit and which is the conventional behaviour there. That includes a schema whose $schema is missing or unrecognized, which falls back to Draft 7 unless defaultDialect names another dialect.

The adapter exposes no format assertion option of its own, so for the schema’s own keywords the detected dialect alone decides. The format keywords inside a published metaschema reached through $ref are asserted in every dialect.

validateAt(data, pointer) validates the full instance and filters errors to the requested pointer and its descendants. It does not compile or validate an isolated subschema.

These capabilities describe the public @texaryn/schema-json adapter. The private Hyperjump adapter is used to test the schema port abstraction and is not the adapter installed by the getting started guide.

Continue with Getting started to create a form, or try schemas in the playground.