Recursive Schemas
Recursive schema graphs are supported at runtime.
The important distinction is that the library follows the graph of Schema instances, not just one level of nesting. That means a field or array item can point back to the same schema instance, and the runtime will keep validating deeper paths using the same operation rules it would use for any non-recursive nested contract.
The practical setup rule is simple: self-references are usually wired after the first createSchema(...) call, because the variable must exist before another field can point at it.
Self-recursive object and array example
const nodeSchema = createSchema({
id: { type: 'string', required: true },
label: { type: 'string', required: true },
parent: { type: 'object', required: false },
children: { type: 'array', required: false }
})
nodeSchema.structure.parent.schema = nodeSchema
nodeSchema.structure.children.items = nodeSchemaThat creates two different recursive edges:
parentis a nested object field that points back tonodeSchemachildren.itemsis an array ofnodeSchemaobjects
Recursive runtime semantics
The same rules still apply inside the recursive graph:
- nested object fields such as
parentinherit the active operation contract - array items that are object schemas still use
replacesemantics - recursive errors stay in the same flat dotted-path shape as any other nested error
Example:
const patchParent = nodeSchema.patch({
parent: {
label: ' Root '
}
})
const patchChildren = nodeSchema.patch({
children: [
{ label: 'Only child label' }
]
})patchParent succeeds with:
{
validatedObject: {
parent: {
label: 'Root'
}
},
errors: {}
}That happens because parent is a nested object field and inherits the outer patch operation.
patchChildren returns:
{
validatedObject: {
children: [
{
label: 'Only child label'
}
]
},
errors: {
'children.0.id': {
field: 'children.0.id',
code: 'REQUIRED',
message: 'Field is required',
params: {}
}
}
}That happens because array items that point to object schemas are always treated as full replacements.
Recursive paths, introspection, and transport export
Recursive schemas keep the same dotted-path model everywhere else too:
nodeSchema.getFieldDefinition('children.0.label')resolves correctlynodeSchema.validateAt('children.0.label', payload)validates only that selected path- recursive transport export is graph-aware rather than stack-recursive
toJsonSchema() uses draft-07 definitions plus $ref for recursive nested contracts, and direct self-recursive object fields point back to #. The transport-specific details are covered again in the Transport JSON Schema Export chapter below.