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.
Supported dialects
Section titled “Supported dialects”| 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.
Form generation capabilities
Section titled “Form generation capabilities”The adapter projects schemas and current data into fields, constraints and annotations for the renderer. The following describes that projection scope, separately from validation.
| Capability | Draft 4 | Draft 7 | 2019-09 | 2020-12 | Form behavior |
|---|---|---|---|---|---|
| Objects and fields | type, properties, required | type, properties, required | type, properties, required | type, properties, required | Declared properties appear even before data is entered. |
| Local references | $ref, definitions | $ref, definitions | $ref, $defs | $ref, $defs | Referenced field schemas are resolved locally; a recursive reference expands once past the data. |
| Composition | allOf, anyOf, oneOf | allOf, anyOf, oneOf | allOf, anyOf, oneOf | allOf, anyOf, oneOf | allOf contributes all branches; anyOf/oneOf selection depends on current data. |
| Conditionals | — | if, then, else | if, then, else | if, then, else | Inactive branch fields remain in the projection with active: false. |
| Dependencies | dependencies | dependencies | dependentSchemas, dependentRequired | dependentSchemas, dependentRequired | Present trigger properties activate dependent fields or required flags. |
| Homogeneous arrays | items | items | items | items | Item fields are projected for entries present in the data. |
| Choices | enum | enum | enum | enum | Enum values become options for the renderer. |
| String constraints | minLength, maxLength, pattern | minLength, maxLength, pattern | minLength, maxLength, pattern | minLength, maxLength, pattern | Constraints are exposed to widgets. |
| Numeric constraints | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf | Constraints are exposed to widgets. |
| Array constraints | minItems, maxItems, uniqueItems | minItems, maxItems, uniqueItems | minItems, maxItems, uniqueItems | minItems, maxItems, uniqueItems | Constraints are exposed in the projection. |
| Field annotations | title, description, default, format | title, description, default, examples, readOnly, writeOnly, format | title, description, default, examples, readOnly, writeOnly, format, deprecated | title, description, default, examples, readOnly, writeOnly, format, deprecated | Annotations 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
itemsin older drafts orprefixItemsin 2020-12) do not have a dedicated projection path. Use homogeneous arrays for generated item controls. patternPropertiesandadditionalPropertiesdo not generate fields for arbitrary object keys. Declare editable fields inproperties.- Boolean schemas and schemas without a resolvable type do not produce
ordinary field nodes. For a
typearray, projection uses the first recognized type rather than creating a type selector. - Unresolved
oneOfobject 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
thenorelsewithoutif, athenunderif: false, anelseunderif: true) are not projected, unless a reference points into that branch or a schema inside it declares an$id,$anchoror$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
$refresources project. Dynamic scope aware projection is opt in as described below.
Recursive references
Section titled “Recursive references”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’schildren.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 inchildren, and the object that carriesbudgetstill 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.
Dynamic reference projection
Section titled “Dynamic reference projection”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.
Initial data and schema defaults
Section titled “Initial data and schema defaults”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.
Validation scope
Section titled “Validation scope”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.