JsonTology.materialize
Construction helper. Use materialize when you produce the data yourself - test fixtures, form scaffolding, default-filled instances. Validates the result by default and throws MaterializationError if validation fails. Pass { enablePartial: true } to allow missing required-without-default fields for lenient construction.
Declaration. Builds a fully-populated instance by merging optional partial input with the schema's declared default values. Returns a MaterializedSchemaType<TSchema> — a plain mutable type, like every schema-derived type in json-tology (see Mutability). Validates the merged result; throws MaterializationError on failure. Pass { enablePartial: true } for lenient construction that accepts missing required fields. Does not strip unknown properties from partial input (partial is trusted); use instantiate for untrusted input.
Use this when you have trusted partial data (from a factory, a test fixture, an admin form) and want the missing fields filled in from schema defaults. The canonical use case: creating a new entity with some known fields, leaving the rest to defaults.
Don't use this when the input is untrusted or may carry unknown properties (use instantiate instead - it validates, strips unknowns, and applies defaults). Don't use it when you want a completely blank instance with zero-values (use jt.value.create instead).
Examples
Example 1: Build a new Book from required fields only
currency and inStock have declared defaults - they are filled in automatically.
/**
* materialize — Example 1: Build a Book from partial data with defaults
* Demonstrates: defaults filled, missing fields absent, vs value.create
*
* The Book here is Michael Ende's Momo (Thienemann Verlag, 1973),
* a sibling title to the canonical Neverending Story rare-book fixture.
*
* BookSchema is an allOf-composed schema (Compose.subClassOf). Both materialize
* and value.create handle composition correctly: materialize validates and fills
* declared defaults from partial data; value.create synthesizes a full zero-value
* instance for all required fields across inherited and own properties.
*/
import type { Book } from '../bookstore/index.js';
import {
BibliographicRecordSchema, BookSchema, bookstoreEntities
} from '../bookstore/index.js';
// Materialize with required fields supplied — defaults filled automatically.
// The partial input carries plain (unbranded) literals — branding happens
// during materialization — so the result is typed via the registry's Book.
const book = bookstoreEntities.materialize(BookSchema, {
'authors': ['Michael Ende'],
'isbn': '9783522115056',
'price': {
'amount': 16.99,
'currency': 'EUR'
},
'printStatus': 'inPrint',
'title': 'Momo'
}) as Book;
console.assert(book.inStock === true);
console.assert(book.isbn === '9783522115056');
console.assert(book.title === 'Momo');
console.log('book.title:', book.title);
console.log('book.isbn:', book.isbn);
console.log('book.inStock (default):', book.inStock);
console.log('book.price:', JSON.stringify(book.price));
// value.create synthesizes zero-values for ALL required fields + explicit defaults.
// BookSchema is allOf-composed — value.create traverses $ref parents and inline
// members, merging inherited (isbn, title, authors) with own (inStock default, etc.).
const bookBlank = bookstoreEntities.value.create(BookSchema.$id) as Record<string, unknown>;
console.assert(bookBlank.isbn === '');
console.assert(bookBlank.inStock === true);
console.log('bookBlank.isbn (zero-value from inherited BibliographicRecord):', bookBlank.isbn);
console.log('bookBlank.inStock (declared default from Book):', bookBlank.inStock);
// Flat schema — value.create works as before.
const biblioBlank = bookstoreEntities.value.create(BibliographicRecordSchema.$id) as Record<string, unknown>;
console.assert((biblioBlank as { 'isbn': string }).isbn === '');
console.log('biblioBlank.isbn (zero-value from BibliographicRecordSchema):', (biblioBlank as { 'isbn': string }).isbn);
Example 2: Materialize a Customer - addresses default is empty array
/**
* materialize — Example 2: Materialize a Customer — addresses default is empty array
* Demonstrates: declared default [] applied automatically, partial is trusted
*
* Bastian Balthazar Bux registers with the bookstore without supplying an
* addresses list — the declared default [] is filled in automatically by
* materialize().
*/
import {
aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
const customer = bookstoreEntities.materialize(CustomerSchema, {
'customerId': aboxFixtures.customer.customerId,
'email': aboxFixtures.customer.email,
'name': aboxFixtures.customer.name
// addresses omitted — declared default [] applied
});
// Declared default [] is applied automatically.
console.assert(Array.isArray(customer.addresses));
console.assert(customer.addresses.length === 0);
console.assert(customer.email === aboxFixtures.customer.email);
console.log('customer name:', customer.name);
console.log('customer email:', customer.email);
console.log('addresses (default applied):', customer.addresses);
Example 3: Contrast with coerce and value.create
/**
* materialize — Example 3: Contrast with instantiate and value.create
* Demonstrates: three distinct construction paths and their trade-offs
*
* Three different ways to build a Book instance from partial data. The canonical
* Neverending Story rare-book fixture provides the required fields.
*
* value.create on a Compose.subClassOf schema (allOf-composed) now synthesizes
* a full instance by resolving inherited fields from each allOf member, including
* $ref parents, and merging them with the child's own fields. Declared defaults
* (e.g. inStock: true) are applied during synthesis.
*/
import {
aboxFixtures, BibliographicRecordSchema, BookSchema, bookstoreEntities
} from '../bookstore/index.js';
// materialize — fills declared defaults, partial is trusted.
const materialized = bookstoreEntities.materialize(BookSchema, {
'authors': [...aboxFixtures.rareBook.authors],
'isbn': aboxFixtures.rareBook.isbn,
'price': aboxFixtures.rareBook.price,
'printStatus': aboxFixtures.rareBook.printStatus,
'title': aboxFixtures.rareBook.title
// inStock omitted — declared default true applied
});
console.assert((materialized as { 'inStock': boolean }).inStock);
// value.create — fills ALL required fields with zero-values + explicit defaults.
// BookSchema is allOf-composed (Compose.subClassOf); value.create traverses all
// allOf members, resolves $ref parents, and merges inherited + own fields.
const bookBlank = bookstoreEntities.value.create(BookSchema.$id) as Record<string, unknown>;
// Inherited from BibliographicRecordSchema (required: isbn, title, authors)
console.assert(bookBlank.isbn === '');
console.assert(bookBlank.title === '');
console.assert(Array.isArray(bookBlank.authors));
// Own fields with declared default: inStock: true
console.assert(bookBlank.inStock === true);
// BibliographicRecordSchema is flat — value.create still works as before.
const blank = bookstoreEntities.value.create(BibliographicRecordSchema.$id) as Record<string, unknown>;
console.assert((blank as { 'isbn': string }).isbn === '');
console.assert((blank as { 'title': string }).title === '');
console.assert(Array.isArray((blank as { 'authors': string[] }).authors));
// instantiate — validates, strips unknowns, applies defaults, throws on failure.
const coerced = bookstoreEntities.instantiate(BookSchema, {
'authors': aboxFixtures.rareBook.authors,
'inStock': aboxFixtures.rareBook.inStock,
'isbn': aboxFixtures.rareBook.isbn,
'price': aboxFixtures.rareBook.price,
'printStatus': aboxFixtures.rareBook.printStatus,
'title': aboxFixtures.rareBook.title
});
console.assert((coerced as { 'isbn': string }).isbn === aboxFixtures.rareBook.isbn);
console.log('materialize inStock (default applied):', (materialized as { 'inStock': boolean }).inStock);
console.log('value.create(BookSchema) isbn (zero-value):', bookBlank.isbn);
console.log('value.create(BookSchema) inStock (default):', bookBlank.inStock);
console.log('value.create(BibliographicRecordSchema) isbn (zero-value):', (blank as { 'isbn': string }).isbn);
console.log('instantiate isbn:', (coerced as { 'isbn': string }).isbn);
Bad examples - what NOT to do
Anti-pattern 1: Using materialize for untrusted API input
/**
* materialize — Example 3: Contrast with instantiate and value.create
* Demonstrates: three distinct construction paths and their trade-offs
*
* Three different ways to build a Book instance from partial data. The canonical
* Neverending Story rare-book fixture provides the required fields.
*
* value.create on a Compose.subClassOf schema (allOf-composed) now synthesizes
* a full instance by resolving inherited fields from each allOf member, including
* $ref parents, and merging them with the child's own fields. Declared defaults
* (e.g. inStock: true) are applied during synthesis.
*/
import {
aboxFixtures, BibliographicRecordSchema, BookSchema, bookstoreEntities
} from '../bookstore/index.js';
// materialize — fills declared defaults, partial is trusted.
const materialized = bookstoreEntities.materialize(BookSchema, {
'authors': [...aboxFixtures.rareBook.authors],
'isbn': aboxFixtures.rareBook.isbn,
'price': aboxFixtures.rareBook.price,
'printStatus': aboxFixtures.rareBook.printStatus,
'title': aboxFixtures.rareBook.title
// inStock omitted — declared default true applied
});
console.assert((materialized as { 'inStock': boolean }).inStock);
// value.create — fills ALL required fields with zero-values + explicit defaults.
// BookSchema is allOf-composed (Compose.subClassOf); value.create traverses all
// allOf members, resolves $ref parents, and merges inherited + own fields.
const bookBlank = bookstoreEntities.value.create(BookSchema.$id) as Record<string, unknown>;
// Inherited from BibliographicRecordSchema (required: isbn, title, authors)
console.assert(bookBlank.isbn === '');
console.assert(bookBlank.title === '');
console.assert(Array.isArray(bookBlank.authors));
// Own fields with declared default: inStock: true
console.assert(bookBlank.inStock === true);
// BibliographicRecordSchema is flat — value.create still works as before.
const blank = bookstoreEntities.value.create(BibliographicRecordSchema.$id) as Record<string, unknown>;
console.assert((blank as { 'isbn': string }).isbn === '');
console.assert((blank as { 'title': string }).title === '');
console.assert(Array.isArray((blank as { 'authors': string[] }).authors));
// instantiate — validates, strips unknowns, applies defaults, throws on failure.
const coerced = bookstoreEntities.instantiate(BookSchema, {
'authors': aboxFixtures.rareBook.authors,
'inStock': aboxFixtures.rareBook.inStock,
'isbn': aboxFixtures.rareBook.isbn,
'price': aboxFixtures.rareBook.price,
'printStatus': aboxFixtures.rareBook.printStatus,
'title': aboxFixtures.rareBook.title
});
console.assert((coerced as { 'isbn': string }).isbn === aboxFixtures.rareBook.isbn);
console.log('materialize inStock (default applied):', (materialized as { 'inStock': boolean }).inStock);
console.log('value.create(BookSchema) isbn (zero-value):', bookBlank.isbn);
console.log('value.create(BookSchema) inStock (default):', bookBlank.inStock);
console.log('value.create(BibliographicRecordSchema) isbn (zero-value):', (blank as { 'isbn': string }).isbn);
console.log('instantiate isbn:', (coerced as { 'isbn': string }).isbn);
Comparison
jt.materialize(BookSchema, {
isbn: '9783522128001', title: 'Die unendliche Geschichte',
authors: ['Michael Ende'], price: 14.99,
})
// → currency: 'USD', inStock: true applied from declared defaults// Zod applies defaults during parse(); no separate materialize step:
const book = BookSchema.parse({
isbn: '9783522128001', title: 'Die unendliche Geschichte',
authors: ['Michael Ende'], price: 14.99,
});
// Requires .default() on each field in the schema definition.import * as v from 'valibot';
// Valibot applies defaults during v.parse() via v.optional(schema, default):
const book = v.parse(BookSchema, {
isbn: '9783522128001', title: 'Die unendliche Geschichte',
authors: ['Michael Ende'], price: 14.99,
});
// Limitation: no separate materialize step; defaults only flow when
// fields are wrapped in v.optional(..., defaultValue).import * as t from 'io-ts';
// Limitation: io-ts has no materialize step and no native default-value
// mechanism. Codecs are values, not registry entries; merge defaults by
// hand before calling .decode():
const defaults = { currency: 'USD', inStock: true };
const result = BookCodec.decode({ ...defaults, ...partial });import { Value } from '@sinclair/typebox/value';
// Value.Default fills defaults:
const book = Value.Default(BookSchema, {
isbn: '9783522128001', title: '...', authors: [...], price: 14.99,
});// AJV applies defaults during validation with { useDefaults: true }:
const ajv = new Ajv({ useDefaults: true });
const data = { isbn: '...', title: '...', authors: [...], price: 14.99 };
ajv.validate(bookSchema, data); // mutates data in place with defaultsbook = Book(isbn='9783522128001', title='Die unendliche Geschichte',
authors=['Michael Ende'], price=14.99)
# currency and in_stock filled from defaults automatically// 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.instantiate- validate + apply defaults + strip unknowns (for untrusted input)jt.value.create- zero-value instance for blank form initializationCompose.getDefaults- extract declared defaults without building an instance- Computed fields - computed properties also run after materialization
See also
- Bookstore domain - where
BookSchemaandCustomerSchemaare defined