Skip to content
Logo

Objects

T.object({ ... }) is the primitive nearly everything else composes inside. This page covers the parts of it that go beyond a flat field list: nested objects, computed fields, sequences, and per-call overrides.

Nesting

Fields can be any schema, including another T.object:

T.object({
  name: T.string.whereby({ length: { min: 1, max: 25 } }),
  pricing: T.object({
    currency: T.enum.uniform(["USD", "YEN", "GBP"]),
    amount: T.number.whereby({ min: 0, max: 500 }),
  }),
});

Every field — nested or not — draws from its own independent stream, assigned when the Fabricator is built. See Reproducibility for what that guarantees.

Sequences

T.object({ id: T.number.integer.sequence });

A monotonically increasing counter starting at 1, fresh per new Fabricator(schema) — every .fabricate() call on the same Fabricator instance advances it, so ten calls yield ids 1 through 10.

Computed fields

A computed field derives its value from the rest of the object, once the rest has already been fabricated. It's reached through .refine(({ compute }) => ({ ... })), not through T.object directly:

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

compute(source) takes a schema (or a bare builder like T.string, when you just need to name the field's type and don't care about its length/range) and returns something with a single method, .as(resolve). resolve receives { fabricated } — the rest of the object, already built — and returns the computed value.

This is why .refine() needs its own method distinct from .extend(): a computed field genuinely needs a second pass, after the ordinary fields resolve. See Composition for the full extend/refine/override picture and the performance trade-off between the two.

Per-call overrides

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

Every field you don't name still comes from the Fabricator's own already-drawn randomness — this isn't a new schema, and it doesn't touch any other field's stream. Values are deep-merged into the fabricated result. To force an omittable or optional field off, pass the exported Omitted sentinel:

import { Omitted } from "@ghostry/fabricator";
 
Product.fabricate({ nickname: Omitted });