Skip to content

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);
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

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}`);
}
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

Comparison

ts
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');
ts
// 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.
ts
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.
ts
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.
ts
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.
py
# 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.
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in io-ts. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// 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);
Output
Press Execute to run this example against the real library.

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.

See also

Released under the MIT License.