Skip to content

Compose.getDefaults Runtime

Validation modes: Validation modes reference

Declaration. Extracts declared default values from a schema's properties, returning a plain object containing only the properties that have a default keyword declared. Properties without defaults are omitted. Nested object properties with their own default-bearing children are recursively traversed.

Use this when you want to pre-populate form fields with schema defaults before the user has provided any input - without running a full coerce that would fail on missing required fields. This is the lightweight alternative to jt.materialize(schema) when you only want explicit defaults and not zero-value synthesis.

Don't use this when you want zero-values for all required fields (use jt.value.create). Don't use it when you want a full materialized instance (use jt.materialize).

Examples

Example 1: Pre-populate a new Book form

BookSchema has currency: 'USD' and inStock: true. isbn, title, authors, and price have no declared defaults - they are omitted from the result.

/**
 * Compose.getDefaults — Example 1: Pre-populate form state from canonical schemas
 *
 * `Compose.getDefaults(schema)` walks the schema's `properties` and
 * returns the subset whose `default` keyword is set. Properties
 * without an explicit default are absent from the result. Nested
 * object properties recurse.
 *
 * The canonical bookstore puts `default: true` on `Book.inStock` and
 * `default: []` on `Customer.addresses` — exercising both the scalar
 * and array-default cases.
 */

import { Compose } from '../../../src/index.js';
import {
  BookSchema, CustomerSchema
} from '../bookstore/index.js';

const bookDefaults = Compose.getDefaults(BookSchema);

console.assert(
  (bookDefaults as { 'inStock'?: boolean }).inStock === true,
  'Book.inStock has default true'
);

const customerDefaults = Compose.getDefaults(CustomerSchema);

console.assert(
  Array.isArray((customerDefaults as { 'addresses'?: readonly unknown[] }).addresses),
  'Customer.addresses has default []'
);

// Form scaffolding: start from declared defaults, leave required fields empty
// so the user fills them in.
const formState = {
  ...bookDefaults,
  'authors': [] as readonly string[],
  'inStock': true,
  'isbn': '',
  'price': {
    'amount': 0,
    'currency': 'EUR'
  },
  'printStatus': 'inPrint' as const,
  'title': ''
};

console.assert(formState.inStock);
console.assert(formState.title === '');
console.assert(formState.authors.length === 0);
console.assert(formState.isbn === '');
console.assert(formState.price.amount === 0);
console.assert(formState.price.currency === 'EUR');
console.assert(typeof formState.printStatus === 'string');
console.log('Book defaults:', bookDefaults);
console.log('Customer defaults:', customerDefaults);
console.log('Form state seeded from defaults:', formState);
Output
Press Execute to run this example against the real library.

Example 2: Pre-populate an Order form

/**
 * Compose.getDefaults — Example 2: Pre-populate an Order form
 *
 * OrderSchema's top-level properties all $ref into primitives that do
 * not declare a default. The extracted defaults object reflects that
 * — every required field stays absent for the user to fill in.
 */

import { Compose } from '../../../src/index.js';
import { OrderSchema } from '../bookstore/index.js';

const defaults = Compose.getDefaults(OrderSchema);

console.assert(typeof defaults === 'object');
// orderId, customerId, orderLines, orderTotal, placedAt, shippingAddress have no
// declared default — none appear in the result.
console.assert(!('orderId' in defaults));
console.assert(!('customerId' in defaults));
console.assert(!('orderTotal' in defaults));
console.log('OrderSchema defaults (empty — all fields are user-supplied):', defaults);
Output
Press Execute to run this example against the real library.

Example 3: Nested defaults are traversed

/**
 * Compose.getDefaults — Example 3: Nested defaults are traversed
 *
 * Nested object properties with their own `default`-bearing children
 * recurse — each nested level contributes its declared defaults to
 * the result. Demonstrates the recursive walk against a settings
 * schema that mirrors the canonical bookstore's notification model.
 */

import { Compose } from '../../../src/index.js';

const NotificationSettingsSchema = {
  '$id': 'https://bookstore.example/NotificationSettings',
  'properties': {
    'notifications': {
      'properties': {
        'email': {
          'default': true,
          'type': 'boolean'
        },
        'push': {
          'default': false,
          'type': 'boolean'
        }
      },
      'type': 'object'
    },
    'theme': {
      'default': 'light',
      'type': 'string'
    }
  },
  'type': 'object'
} as const;

const defaults = Compose.getDefaults(NotificationSettingsSchema) as {
  'notifications'?: { 'email'?: boolean;
    'push'?: boolean };
  'theme'?: string;
};

console.assert(defaults.theme === 'light');
console.assert(defaults.notifications?.email === true);
console.assert(defaults.notifications?.push === false);
console.log('NotificationSettings defaults (nested recursion):', defaults);
Output
Press Execute to run this example against the real library.

Bad examples - what NOT to do

Anti-pattern 1: Using getDefaults as a substitute for coerce/materialize

/**
 * Compose.getDefaults — Anti-pattern: Treating defaults as a full instance
 *
 * `getDefaults` returns only declared defaults — never synthesised
 * zero-values for required fields. For a blank-but-valid Customer
 * scaffold, use `bookstoreEntities.value.create` instead.
 */

import {
  bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';

// ✓ Do this — value.create synthesises zero-values for required fields,
//   along with declared defaults (here, `addresses: []`).
const blank = bookstoreEntities.value.create(CustomerSchema.$id) as Record<string, unknown>;

console.assert(Array.isArray(blank.addresses));
console.assert(typeof blank.customerId === 'string');
console.assert(typeof blank.email === 'string');
console.assert(typeof blank.name === 'string');
console.log('value.create synthesises all required fields:', {
  'addresses': blank.addresses,
  'customerId': blank.customerId,
  'email': blank.email,
  'name': blank.name
});
Output
Press Execute to run this example against the real library.

Comparison

ts
Compose.getDefaults(BookSchema)
// { currency: 'USD', inStock: true }
// Only properties with declared `default` values
ts
// Zod has no built-in getDefaults. Closest workaround:
// schema.parse({}) fails on missing required fields.
// Manual extraction hardcodes default values:
const defaults = { currency: 'USD', inStock: true };
// Limitation: hardcoded, not derived from the schema. Must be updated manually
// every time a default changes. No recursive traversal of nested objects.
ts
import * as v from 'valibot';
// Limitation: Valibot has no exposed default-extraction helper.
// Defaults attached via v.optional(schema, defaultValue) are applied
// during v.parse but are not enumerable by a public API.
const defaults = { currency: 'USD', inStock: true }; // hardcoded
ts
// Limitation: io-ts has no native default-value mechanism. Codecs decode
// inputs as-is; missing properties produce decode errors. Default values
// must be merged in by hand before calling .decode():
const defaults = { currency: 'USD', inStock: true };
const result = BookCodec.decode({ ...defaults, ...input });
ts
import { Value } from '@sinclair/typebox/value';
// Value.Default fills ALL fields including zero-values, not just declared defaults:
Value.Default(BookSchema, {})
// → { isbn: undefined, title: undefined, ..., currency: 'USD', inStock: true }
// More fields than getDefaults
ts
// AJV has no built-in getDefaults utility.
// Closest workaround: validate an empty object with { useDefaults: true }:
const ajv = new Ajv({ useDefaults: true });
const draft = {};
ajv.validate(bookSchema, draft);
// draft now has defaults injected into it.
// Limitation: modifies the input object in place; requires a full validate call;
// does not return only the declared defaults - fills them into an existing object.
py
# Extract defaults from model fields:
from pydantic._internal._fields import PydanticUndefined

defaults = {
    name: field.default
    for name, field in Book.model_fields.items()
    if field.default is not PydanticUndefined
}
# { 'currency': 'USD', 'in_stock': True }
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.
  • jt.materialize - build a full instance from partial data + defaults
  • jt.value.create - synthesize zero-values for all required fields + explicit defaults

See also

Released under the MIT License.