Skip to content

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

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

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

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

Comparison

ts
jt.materialize(BookSchema, {
  isbn: '9783522128001', title: 'Die unendliche Geschichte',
  authors: ['Michael Ende'], price: 14.99,
})
// → currency: 'USD', inStock: true applied from declared defaults
ts
// 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.
ts
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).
ts
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 });
ts
import { Value } from '@sinclair/typebox/value';
// Value.Default fills defaults:
const book = Value.Default(BookSchema, {
  isbn: '9783522128001', title: '...', authors: [...], price: 14.99,
});
ts
// 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 defaults
py
book = Book(isbn='9783522128001', title='Die unendliche Geschichte',
            authors=['Michael Ende'], price=14.99)
# currency and in_stock filled from defaults automatically
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 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.

See also

Released under the MIT License.