Skip to content
Logo

Composition

Three ways to derive one schema from another, plus one way to override an already-built Fabricator for a single call. They compose with each other.

.extend() — add or replace fields

const IdentifiableProductSchema = ProductSchema.extend(({ base }) => ({
  id: T.number.integer.sequence,
}));

Combines the base schema's field definitions with the ones you provide. The result is deep-merged: a new field is added, and a field with the same name as an existing one replaces it wholesale (nested objects merge; anything else overrides). base is the original definition, handed to the extender in case the new fields need to reference it — return {} if you don't need it.

.refine() — add fields computed from fabricated data

const WithSlug = ProductSchema.refine(({ compute }) => ({
  slug: compute(T.string).as(({ fabricated }) =>
    fabricated.name.toLowerCase().replace(/\s+/g, "-"),
  ),
}));

Same shape as .extend(), but the new fields are computed from the fabricated result of the base schema rather than defined statically — see Objects for the full compute API. Refine stages chain: a later .refine() can reference a field a prior one computed.

.override() — bake fixed values into a schema

const WidgetSchema = ProductSchema.override({
  name: "Widget",
  pricing: { currency: "USD" },
});
 
const widget = new Fabricator(WidgetSchema).fabricate();

Pure Schema → Schema — never touches randomness. The named fields are still fully dispatched when the resulting schema is built (so unrelated fields' random streams stay assigned exactly as they would without the override — see Reproducibility), but their draws are discarded in favor of the fixed value. Values deep-merge into the existing definition, and chained overrides compose (.override(a).override(b), with b winning on conflicts).

.fabricate(overrides) — a per-call override, not a new schema

const Product = new Fabricator(ProductSchema);
const widget = Product.fabricate({ name: "Widget" });

The lightweight sibling of .override(): instead of producing a new Schema, it overrides one call's result on an already-built Fabricator, using that Fabricator's own already-drawn randomness for every other field. See Objects for the Omitted sentinel and merge details.

Extending a subtype

const ElectronicsSchema = ProductSchema.extend(() => ({
  category: T.always("electronics"),
}));
 
const laptop = new Fabricator(ElectronicsSchema).fabricate({ name: "Laptop" });

.extend(), .refine(), and .override() all return ordinary Schemas, so they compose with each other and with .fabricate(overrides) freely — a schema-level fixed field (T.always('electronics')) and a per-call override ({ name: 'Laptop' }) are just two different points in the same pipeline.