JsonTology.instantiate Compile-time + Runtime
Validation modes: Validation modes reference
Trust boundary. Use instantiate when data crosses into your system from outside - HTTP request bodies, queue messages, file imports, IPC payloads. Failure means the caller sent invalid data; InstantiationError carries the full structured error list for your error response.
Declaration. Runs any registered Transform decoder first, then validates the decoded data against the registered schema, applies default values declared on schema properties, strips unknown properties, and returns a fully typed result. Throws InstantiationError on validation failure. The input is deep-cloned before mutation - the original is never modified. See Canonical decode/default ordering for the full sequence.
Use this when you have an unknown-shape input (a request body, a queue message, a config blob, a database row) and you want a typed, validated, defaults-applied domain object - or a typed exception. This is the right method 80% of the time when data enters your application boundary. Prefer this over calling validate and then mapping fields manually.
Don't use this when you need just a yes/no answer without a throw (use is instead). Don't use it when you want the structured error list without the exception (use validate instead). Don't call instantiate on already-coerced values - the result of instantiate is already clean and typed. Don't use instantiate inside a tight inner loop over millions of calls with a fixed schema - pull jt.registry.validator(schemaId) once and reuse the compiled validator.
Examples
Example 1: Validate and apply defaults
Valid input: unknown properties are stripped, defaults are filled, the return type is Customer.
/**
* coerce — Example 1: Validate and apply defaults
* Demonstrates: valid input, defaults filled, unknowns stripped
*
* Uses the canonical Bastian Balthazar Bux customer fixture, with one
* unknown property added inline so the smoke test sees stripping work.
*/
import {
aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
const customer = bookstoreEntities.instantiate(CustomerSchema.$id, {
'customerId': aboxFixtures.customer.customerId,
'email': aboxFixtures.customer.email,
'internalNotes': 'vip',
'name': aboxFixtures.customer.name
// addresses omitted — default [] will be applied
});
// customer is typed as Customer
console.assert(customer.name === aboxFixtures.customer.name);
console.assert(Array.isArray(customer.addresses));
// addresses is optional in the type; instantiate fills the schema default at runtime.
console.assert((customer.addresses ?? []).length === 0);
console.assert(!('internalNotes' in customer));
console.log('name:', customer.name);
console.log('addresses (default applied):', customer.addresses);
console.log('internalNotes stripped:', !('internalNotes' in customer));
Example 2: Coerce as part of a request handler
Catch InstantiationError and convert to an RFC 7807 Problem Details response (built on errors.report).
/**
* instantiate — Example 2: Coerce as part of a request handler
* Demonstrates: InstantiationError catch pattern, .errors.report() RFC 7807
*
* Simulates an HTTP handler receiving invalid customer data and returning
* a structured 422 response rather than an uncaught exception.
*/
import { InstantiationError } from '../../../src/index.js';
import {
bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
function createCustomer(body: unknown): unknown {
try {
return bookstoreEntities.instantiate(CustomerSchema.$id, body);
} catch (error) {
if (error instanceof InstantiationError) {
return {
'body': error.errors.report({ 'instance': '/customers' }),
'status': 422
};
}
throw error;
}
}
// Invalid body: email is not an email, name is a number
const result = createCustomer({
'email': 'not-an-email',
'id': 'c1a2b3d4-e5f6-7890-abcd-ef1234567890',
'name': 42
});
console.assert(typeof result === 'object' && result !== null);
console.assert((result as { 'status': number }).status === 422);
console.assert(typeof (result as { 'body': unknown }).body === 'object');
console.log('status:', (result as { 'status': number }).status);
console.log('body:', JSON.stringify((result as { 'body': unknown }).body, null, 2));
Example 3: Coerce a nested schema with $ref
OrderSchema contains items: [OrderLine] via $ref. Each OrderLine is coerced independently. See the bookstore domain for schema definitions.
/**
* instantiate — Example 3: Coerce a nested schema with $ref (Order → OrderLine)
* Demonstrates: nested coercion via $ref, unknown field stripping at each level
*
* Bastian Balthazar Bux orders a single copy of the 1979 first edition.
* The `extra` field on the OrderLine is stripped; `unexpectedField` on Order
* is also stripped. Total matches invariant (850 × 1 = 850).
*/
import {
aboxFixtures, bookstoreEntities, OrderSchema
} from '../bookstore/index.js';
const order = bookstoreEntities.instantiate(OrderSchema.$id, {
'customerId': aboxFixtures.customer.customerId,
'orderId': aboxFixtures.order.orderId,
'orderLines': [{
// unknown field — stripped from OrderLine
'bookIsbn': aboxFixtures.rareBook.isbn,
'extra': 'gone',
'quantity': 1,
'unitPrice': aboxFixtures.rareBook.price
}],
// 850 EUR × 1 — satisfies invariant
'orderTotal': aboxFixtures.rareBook.price,
'placedAt': '2026-01-15T10:30:00Z',
'shippingAddress': aboxFixtures.order.shippingAddress,
// unknown field — stripped from Order
'unexpectedField': 'stripped'
});
console.assert(order.orderLines.length === 1);
console.assert(!('extra' in order.orderLines[0]));
console.assert(!('unexpectedField' in order));
console.log('orderLines count:', order.orderLines.length);
console.log('extra field stripped from line:', !('extra' in order.orderLines[0]));
console.log('unexpectedField stripped from order:', !('unexpectedField' in order));
Bad examples - what NOT to do
Anti-pattern 1: Catching InstantiationError silently
/**
* instantiate — Anti-pattern 1: Catching InstantiationError silently
* Demonstrates: the bad swallow pattern vs surfacing the error list via validate
*
* Invalid customer body: missing id, name is absent — the error list is lost
* in the anti-pattern. The correct path uses validate() to surface items.
*/
import {
bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
const invalidBody = { 'email': 'walter.moers@bookstore.example' };
// Anti-pattern: swallowing InstantiationError loses structured errors
// Don't do this
try {
bookstoreEntities.instantiate(CustomerSchema.$id, invalidBody);
} catch {
// swallowed — errors lost
}
// Correct approach: use validate() to surface the structured error list
const errs = bookstoreEntities.validate(CustomerSchema.$id, invalidBody);
if (!errs.ok) {
const messages = errs.items.map((err) => {
return `${err.path}: ${err.message}`;
});
console.assert(Array.isArray(messages));
console.assert(messages.length > 0);
console.log('validation errors:', messages);
}
console.assert(!errs.ok);
Anti-pattern 2: Coercing already-coerced values
/**
* instantiate — Anti-pattern 2: Coercing already-coerced values
* Demonstrates: the wasteful double-instantiate vs using the first result
*
* Bastian Balthazar Bux — valid customer used to show that re-coercing
* an already-coerced value is redundant work.
*/
import {
aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
// Anti-pattern: double instantiation — wasted work
// Don't do this
const first = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);
// redundant — first is already coerced and typed
const _again = bookstoreEntities.instantiate(CustomerSchema, first);
void _again;
// Correct approach: use the first result directly
const customer = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);
console.assert(customer.name === aboxFixtures.customer.name);
console.assert(customer.email === aboxFixtures.customer.email);
console.assert(Array.isArray(customer.addresses));
console.log('name:', customer.name, ', email:', customer.email);
Anti-pattern 3: Building partial shapes by hand instead of using derived schemas
/**
* instantiate — Anti-pattern 3: Building partial shapes by hand
* Demonstrates: manual partial object (bad) vs Compose.pick (correct)
*
* A signup endpoint only needs name + email from the full Customer schema.
* Compose.pick produces a proper sub-schema rather than passing a hand-built
* partial object to the full schema validator.
*/
import { Compose } from '../../../src/index.js';
import {
createBookstoreDocRegistry,
CustomerSchema
} 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 signupBody = {
'email': 'cornelia.funke@bookstore.example',
'name': 'Cornelia Funke'
};
// Anti-pattern: building a partial shape by hand and passing to the full schema
// Don't do this
const partial = {
'email': signupBody.email,
'name': signupBody.name
};
// The full CustomerSchema requires `id`, so this would throw InstantiationError
// if used with instantiate directly.
void partial;
// Correct approach: pick the sub-schema, coerce cleanly
const SignupSchema = Compose.pick(
CustomerSchema,
[
'email',
'name'
] as const,
'https://bookstore.example/Signup'
);
jt.set(SignupSchema);
const signup = jt.instantiate(SignupSchema, signupBody);
console.assert(signup.email === signupBody.email);
console.assert(signup.name === signupBody.name);
console.log('signup schema id:', SignupSchema.$id);
console.log('coerced signup:', signup.name, '-', signup.email);
Comparison
import { InstantiationError } from 'json-tology';
const customer = jt.instantiate(CustomerSchema.$id, rawData);
// throws InstantiationError on failure
// typed as Customer
// defaults applied, unknowns stripped, Transform decoders runimport { z } from 'zod';
const CustomerSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string(),
addresses: z.array(z.object({ street: z.string(), city: z.string(), postalCode: z.string() })).default([]),
});
const customer = CustomerSchema.parse(rawData);
// throws ZodError on failure
// typed; .default() fields filled; unknown keys stripped by default (.strip() mode)import * as v from 'valibot';
const CustomerSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
email: v.pipe(v.string(), v.email()),
name: v.pipe(v.string(), v.minLength(1)),
addresses: v.optional(v.array(v.object({
street: v.string(), city: v.string(), postalCode: v.string(),
})), []),
});
const customer = v.parse(CustomerSchema, rawData);
// throws ValiError on failure; typed via v.InferOutput
// Defaults flow through v.optional(schema, defaultValue), not via a registry option.
// Unknown keys stripped by default; no Transform decoder registry.import * as t from 'io-ts';
import { isLeft } from 'fp-ts/Either';
import { PathReporter } from 'io-ts/PathReporter';
const CustomerCodec = t.exact(t.type({
id: t.string,
email: t.string,
name: t.string,
}));
const result = CustomerCodec.decode(rawData);
if (isLeft(result)) {
throw new Error(PathReporter.report(result).join('\n'));
}
const customer = result.right;
// Limitation: no native default-fill, no decoder registry, no
// InstantiationError class. t.exact strips unknown keys; merge defaults by
// hand before decode.import { Value } from '@sinclair/typebox/value';
import { TypeCompiler } from '@sinclair/typebox/compiler';
const C = TypeCompiler.Compile(CustomerSchema);
// Two-step: default-fill, then check
const filled = Value.Default(CustomerSchema, Value.Clone(rawData));
if (!C.Check(filled)) {
throw new Error([...C.Errors(filled)].map(e => e.message).join(', '));
}
const customer = Value.Clean(CustomerSchema, filled);
// No typed InstantiationError; manual process; no Transform decoder supportimport Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv({ useDefaults: true, removeAdditional: true });
addFormats(ajv);
const valid = ajv.validate(customerSchema, rawData);
// rawData mutated in place - no typed return value
if (!valid) throw new Error(ajv.errorsText());
// No TypeScript type narrowing; errors are ajv's ErrorObject[]from pydantic import BaseModel, ValidationError
class Customer(BaseModel):
id: str
email: str
name: str
addresses: list[Address] = []
try:
customer = Customer.model_validate(raw_data)
# defaults applied; extra fields ignored (extra='ignore' default)
except ValidationError as e:
print(e.errors()) # structured error list// 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 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.Related
JsonTology.validate- when you need structuredValidationErrorswithout an exceptionJsonTology.is- when you only need a boolean type guardJsonTology.materialize- when you want to build from partial trusted data + defaults without validation throwingCompose.pick/omit- build sub-schemas before passing toinstantiate
See also
- Bookstore domain - where
Customer,Order, andOrderLineare defined - Error views - what to do with the
ValidationErrorswhen instantiate throws - Transforms - how Transform decoders integrate with
instantiate
Per-call options
instantiate accepts an optional third argument to override behavior for a single call:
| Option | Type | Default | Purpose |
|---|---|---|---|
enableDefaults | boolean | inherits from JsonTology.create | Override default-filling for this call only. |
Example: validate without filling defaults
Useful for PATCH endpoints where missing fields mean "no change" rather than "use default":
/**
* instantiate — Per-call option: validate without filling defaults
* Demonstrates: enableDefaults: false for PATCH semantics
*
* On a PATCH endpoint, missing fields mean "no change" rather than
* "apply schema default". Hermann Hesse sends a name-only update;
* addresses must stay missing, not be filled with the schema default [].
*/
import {
bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
const incomingPatchBody = {
'customerId': 'a2b3c4d5-e6f7-8901-bcde-f12345678901',
'email': 'hermann.hesse@bookstore.example',
// addresses intentionally absent — PATCH means "don't change"
'name': 'Hermann Hesse'
};
const patched = bookstoreEntities.instantiate(
CustomerSchema,
incomingPatchBody,
// missing fields stay missing
{ 'enableDefaults': false }
);
console.assert(patched.name === 'Hermann Hesse');
// With enableDefaults: false, addresses is not filled with the default []
// Cast to unknown first to avoid false type-overlap errors at the property check
const patchedRecord = patched as Record<string, unknown>;
console.assert(
!('addresses' in patchedRecord) || patchedRecord.addresses === undefined,
'addresses should not be default-filled on a PATCH call'
);
console.log('name:', patched.name);
console.log('addresses present after PATCH instantiate:', 'addresses' in patchedRecord);
The registry's global enableDefaults setting is unchanged by per-call options.