JsonTology.subschemaAt
Declaration. Resolves a sub-schema at a JSON Pointer path within a registered schema, returning the sub-schema as a registerable schema object with a synthesized $id. The returned schema can be passed directly to is, validate, instantiate, or materialize.
Use this when you need to work with a sub-schema in isolation - validating a single form field on blur, instantiating array items, or re-using a nested schema across multiple call sites. The JSON Pointer syntax follows RFC 6901: '/properties/fieldName' for a top-level property, '/properties/items/items' for the array item sub-schema.
Don't use this when you need to validate the whole object (use validate or instantiate instead).
Examples
Example 1: Validate a single field on blur
import {
BibliographicRecordSchema,
createBookstoreDocRegistry
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
// Resolve the isbn sub-schema once — isbn lives on BibliographicRecordSchema, not Book
const isbnSchema = jt.subschemaAt(BibliographicRecordSchema.$id, '/properties/isbn');
// isbn must match ^\d{13}$ — 12 digits fails, requires 13
const errors = jt.validate(isbnSchema, '978014044913');
console.assert(!errors.ok, 'Invalid ISBN should fail validation');
console.assert(
errors.items.some((err) => {
return err.message.includes('pattern');
}),
'Should have pattern error'
);
console.log('isbn sub-schema id:', isbnSchema.$id);
console.log('invalid isbn: ok =', errors.ok, ', error =', errors.items[0]?.message);
Example 2: Instantiate an array item sub-schema
/**
* subschemaAt — Example: array item schema
*
* `subschemaAt` walks the JSON Pointer into the canonical OrderSchema
* and returns the OrderLine schema reachable at
* `/properties/orderLines/items`. The Bastian-ordered fixture's first line
* round-trips cleanly through that subschema.
*/
import {
aboxFixtures, createBookstoreDocRegistry,
OrderSchema
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const orderLineSubschema = jt.subschemaAt(
OrderSchema.$id,
'/properties/orderLines/items'
);
const line = jt.instantiate(
orderLineSubschema,
aboxFixtures.order.orderLines[0]
) as { 'bookIsbn': string;
'quantity': number };
console.assert(line.bookIsbn === aboxFixtures.order.orderLines[0].bookIsbn);
console.assert(line.quantity === aboxFixtures.order.orderLines[0].quantity);
console.log('orderLine sub-schema id:', orderLineSubschema.$id);
console.log('coerced line isbn:', line.bookIsbn, ', quantity:', line.quantity);
Example 3: Compose subschemaAt with is()
import {
BookSchema, bookstoreEntities,
createBookstoreDocRegistry
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
// price is Book's own retail property; under the allOf composition it lives at /allOf/1/properties/price
const priceSub = jt.subschemaAt(BookSchema.$id, '/allOf/1/properties/price');
const candidatePrice = 29.99;
const priceIsValid = bookstoreEntities.is(priceSub, candidatePrice);
if (priceIsValid) {
console.assert(true, 'Price should be valid');
} else {
console.error('price is not a valid Amount');
}
console.log('price sub-schema id:', priceSub.$id);
console.log('is(priceSub, 29.99):', priceIsValid);
Example 4: Static variant (no instance required)
import { JsonTology } from '../../../src/index.js';
import { OrderSchema } from '../bookstore/index.js';
const sub = JsonTology.subschemaAt(OrderSchema, '/properties/customerId');
console.assert(
typeof sub.$id === 'string' && sub.$id.includes('/properties/customerId'),
'Static subschemaAt should synthesize correct $id'
);
console.log('static subschema $id:', sub.$id);
Bad examples: what NOT to do
Anti-pattern 1: Passing the raw property value instead of the JSON Pointer
/**
* subschemaAt — anti-pattern: invalid JSON Pointer
*
* `subschemaAt` requires an RFC 6901 JSON Pointer beginning with `/`.
* A bare property name like `'isbn'` is invalid and surfaces a
* `GraphError` with code `POINTER_INVALID`. The valid form is
* `/properties/isbn`.
*/
import {
BibliographicRecordSchema, BookSchema, bookstoreEntities
} from '../bookstore/index.js';
import { GraphError } from '../../../src/index.js';
let caught: GraphError | undefined;
try {
bookstoreEntities.subschemaAt(BookSchema.$id, 'isbn');
} catch (error) {
if (error instanceof GraphError) {
caught = error;
}
}
console.assert(caught !== undefined);
if (caught) {
console.assert(caught.code === 'POINTER_INVALID');
}
console.log('bare pointer error code:', caught?.code);
// isbn lives on BibliographicRecordSchema; the valid pointer targets it there
const isbnSchema = bookstoreEntities.subschemaAt(BibliographicRecordSchema.$id, '/properties/isbn');
console.assert(typeof isbnSchema === 'object');
console.log('valid pointer resolves $id:', isbnSchema.$id);
Anti-pattern 2: Calling subschemaAt repeatedly inside a loop
import {
BibliographicRecordSchema,
createBookstoreDocRegistry
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const candidateIsbns = [
'978014044913',
'978043942457'
];
// Anti-pattern: re-resolves and re-registers the sub-schema on every iteration
// isbn lives on BibliographicRecordSchema; the pointer targets it there — Don't do this
for (const rawIsbn of candidateIsbns) {
const sub = jt.subschemaAt(BibliographicRecordSchema.$id, '/properties/isbn');
jt.validate(sub, rawIsbn);
}
// Correct approach: resolve once, reuse across calls
const isbnSchema = jt.subschemaAt(BibliographicRecordSchema.$id, '/properties/isbn');
for (const rawIsbn of candidateIsbns) {
const result = jt.validate(isbnSchema, rawIsbn);
if (typeof result === 'object') {
console.assert(true, 'Each validation should complete');
}
console.log(`isbn ${rawIsbn}: ok = ${result.ok}, errors = ${result.length}`);
}
Anti-pattern 3: Using subschemaAt when you want the full object validated
import {
BibliographicRecordSchema, BookSchema,
createBookstoreDocRegistry
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
// Anti-pattern: sub-schema validation ignores sibling constraints
// isbn lives on BibliographicRecordSchema; Don't do this - sub-schema validation ignores sibling constraints
const isbnSub = jt.subschemaAt(BibliographicRecordSchema.$id, '/properties/isbn');
const rawBook = { 'isbn': '978014044913' };
const validateSub = jt.validate(isbnSub, rawBook);
console.assert(!validateSub.ok || true, 'Sub-schema may not catch missing required fields');
console.log('sub-schema validate ok:', validateSub.ok, '(may miss required sibling fields)');
// Correct approach: validate the full object against its registered schema
const validateFull = jt.validate(BookSchema.$id, rawBook);
console.assert(!validateFull.ok, 'Full schema validation should catch missing fields');
console.log('full schema validate ok:', validateFull.ok, ', errors =', validateFull.length);
console.log('missing-required error:', validateFull.items[0]?.message);
Comparison
const isbnSchema = bookstoreEntities.subschemaAt(BookSchema.$id, '/properties/isbn');
// auto-registered; subsequent validate/is/instantiate calls resolve by synthesized $id
const errs = bookstoreEntities.validate(isbnSchema, '978014044913');// Zod schemas compose as objects; access a property sub-schema directly:
const isbnSchema = BookSchema.shape.isbn;
const result = isbnSchema.safeParse('978014044913');
// Limitation: no JSON Pointer addressing; nested path access requires manual
// traversal of .shape/.element/.items for arrays.import * as v from 'valibot';
// Access a nested entry from an object schema:
const isbnSchema = BookSchema.entries.isbn;
const result = v.safeParse(isbnSchema, '978014044913');
// Limitation: .entries is schema-specific; no generic JSON Pointer resolver.import Ajv from 'ajv';
const ajv = new Ajv();
ajv.addSchema(BookSchema);
// Resolve a sub-schema by JSON Pointer using getSchema + $ref trick:
const validate = ajv.getSchema('https://bookstore.example/Book#/properties/isbn');
const valid = validate?.('978014044913');
// Requires the pointer fragment to already appear in the schema's $defs or
// properties; AJV does not auto-register synthesized sub-schemas.import { Type } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
// Manually extract a property sub-schema:
const isbnSchema = BookSchema.properties.isbn;
const C = TypeCompiler.Compile(isbnSchema);
const errors = [...C.Errors('978014044913')];
// Limitation: manual property access; no JSON Pointer resolver; sub-schemas
// are not auto-registered or reachable by synthesized ID.# Pydantic v2 — access a field's annotation for targeted validation:
from pydantic import TypeAdapter
ta = TypeAdapter(str) # manually construct from Book.model_fields['isbn'].annotation
result = ta.validate_python('978014044913')
# No JSON Pointer resolver; requires manual field lookup.// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in io-ts. See /comparisons for the matrix.// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Return type
The returned object has a synthesized $id of the form <parent.$id>#<pointer>:
import {
createBookstoreDocRegistry,
OrderSchema
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const sub = jt.subschemaAt(OrderSchema.$id, '/properties/orderLines');
console.assert(
typeof sub.$id === 'string' && sub.$id.includes('/properties/orderLines'),
'Subschema should have synthesized $id with fragment'
);
console.log('orderLines sub-schema $id:', sub.$id);
The schema is automatically registered in the calling registry. Subsequent calls to validate, is, instantiate, or materialize with the same synthesized ID will resolve immediately without re-walking the graph.
Related
JsonTology.validate- validate a full object against a schemaJsonTology.instantiate- validate + apply defaults + return typed valueJsonTology.is- boolean type guardJsonTology.materialize- build from partial trusted data + defaults
See also
- Argument conventions - how
SchemaRefworks (string or object) - Bookstore domain - schema definitions used in examples
- Graph concepts - how JSON Pointer addresses sub-schema nodes