Copy-pasteable patterns for common scenarios.
Ad-hoc generation
No world needed. Great for one-off fixtures or test helpers:
import { generate } from "zod4-mock";
const address = generate(AddressSchema);
const users = generate(z.array(UserSchema).min(3).max(10));
// Overrides work too
const admin = generate(UserSchema, { overrides: { role: "admin" } });Run an ad-hoc schema live — edit it and the output regenerates:
Reproducible test data
Wrap world creation in a factory. Every call with the same seed produces identical data:
// fixtures.ts
import { createWorld } from "zod4-mock";
export function makeWorld(seed = 42) {
return createWorld({ seed })
.withSchema(PersonSchema)
.withSchema(DocumentSchema, {
relations: { author: PersonSchema },
});
}
// person.test.ts
const world = makeWorld();
const people = world.generate(z.array(PersonSchema).min(5));
const docs = world.generate(z.array(DocumentSchema).min(10));Custom field values with ctx.gen
Use ctx.gen to plug in generators directly — the PRNG is already applied:
const world = createWorld({ seed: 42 }).withSchema(ProductSchema, {
matchers: {
name: (ctx) => ctx.gen.commerce.productName(),
sku: (ctx) => `SKU-${ctx.gen.string.alphanumeric(6)}`,
description: (ctx) => ctx.gen.word.sentence(),
priceCents: (ctx) => ctx.prng.int(100, 50_000),
},
});Invoicing domain
Customers, products, invoices with mathematically correct line totals, and customer summaries — all referentially consistent.
import { z } from "zod";
import { createWorld } from "zod4-mock";
const CustomerSchema = z.object({
customerId: z.uuid(),
name: z.string(),
email: z.email(),
});
const ProductSchema = z.object({
productId: z.uuid(),
sku: z.string(),
name: z.string(),
unitPriceCents: z.number().int().min(100),
});
const LineItemSchema = z.object({
productId: z.uuid(),
sku: z.string(),
description: z.string(),
quantity: z.number().int().min(1).max(20),
unitPriceCents: z.number().int().min(1),
totalCents: z.number().int().min(1),
});
const InvoiceSchema = z.object({
id: z.uuid(),
customerId: z.uuid(),
invoiceDate: z.date(),
status: z.enum(["draft", "sent", "paid", "overdue"]),
lines: z.array(LineItemSchema).min(1).max(8),
totalCents: z.number().int().min(1),
currency: z.enum(["EUR", "USD", "GBP"]),
});
const CustomerSummarySchema = z.object({
customerId: z.uuid(),
name: z.string(),
email: z.email(),
invoiceCount: z.number().int().min(0),
totalOwedCents: z.number().int().min(0),
});
function createInvoicingWorld(seed = 42) {
// Track computed totals across the lines/totalCents matchers
const lineTotals = new Map<string, number>();
return createWorld({ seed })
.withSchema(ProductSchema, {
matchers: {
sku: (ctx) => `SKU-${ctx.gen.string.alphanumeric(6)}`,
unitPriceCents: (ctx) => ctx.prng.int(1, 500) * 100,
},
})
.withSchema(InvoiceSchema, {
relations: { customer: CustomerSchema },
matchers: {
customerId: (ctx) => ctx.related("customer").customerId,
lines: (ctx) => {
const count = ctx.prng.int(1, 4);
let total = 0;
const lines = Array.from({ length: count }, () => {
const product = ctx.registry.pick(ProductSchema);
const quantity = ctx.prng.int(1, 10);
const lineTotalCents = quantity * product.unitPriceCents;
total += lineTotalCents;
return {
productId: product.productId,
sku: product.sku,
description: product.name,
quantity,
unitPriceCents: product.unitPriceCents,
totalCents: lineTotalCents,
};
});
lineTotals.set(ctx.fieldPath, total);
return lines;
},
totalCents: (ctx) => lineTotals.get(ctx.fieldPath.replace(".totalCents", ".lines")) ?? 1,
},
})
.withSchema(CustomerSummarySchema, {
from: CustomerSchema,
matchers: {
customerId: (ctx) => ctx.source.customerId,
name: (ctx) => ctx.source.name,
email: (ctx) => ctx.source.email,
},
});
}
const world = createInvoicingWorld(42);
const products = world.generate(z.array(ProductSchema).min(10));
const invoices = world.generate(z.array(InvoiceSchema).min(5));
const summaries = world.generate(z.array(CustomerSummarySchema));
// invoices[*].customerId ∈ summaries[*].customerId — guaranteed
// invoices[*].totalCents === sum of invoices[*].lines[*].totalCents — guaranteedDocument corpus
A hierarchy of authors → documents → sentences → annotations with referential integrity throughout.
import { z } from "zod";
import { createWorld } from "zod4-mock";
const AuthorSchema = z.object({
authorId: z.uuid(),
language: z.enum(["nl", "en", "de", "fr"]),
});
const DocumentSchema = z.object({
id: z.uuid(),
authorId: z.uuid(),
language: z.enum(["nl", "en", "de", "fr"]),
title: z.string().min(5).max(80),
text: z.string().min(10).max(300),
});
const SentenceSchema = z.object({
id: z.uuid(),
documentId: z.uuid(),
position: z.number().int().min(0),
text: z.string().min(10).max(200),
});
const AnnotationSchema = z.object({
sentenceId: z.uuid(),
authorId: z.uuid(),
offset: z.number().int().min(0).max(250),
length: z.number().int().min(1).max(50),
label: z.enum(["person", "location", "organisation", "date"]),
});
function createCorpusWorld(seed = 42) {
return createWorld({ seed })
.withSchema(AuthorSchema)
.withSchema(DocumentSchema, {
relations: { author: AuthorSchema },
matchers: {
id: (ctx) => ctx.gen.string.uuid(),
authorId: (ctx) => ctx.related("author").authorId,
language: (ctx) => ctx.related("author").language,
},
})
.withSchema(SentenceSchema, {
relations: { document: DocumentSchema },
matchers: {
documentId: (ctx) => ctx.related("document").id,
},
})
.withSchema(AnnotationSchema, {
relations: { sentence: SentenceSchema, author: AuthorSchema },
matchers: {
sentenceId: (ctx) => ctx.related("sentence").id,
authorId: (ctx) => ctx.related("author").authorId,
},
});
}
// Generate order matters — referenced schemas must exist first
const world = createCorpusWorld(42);
const authors = world.generate(z.array(AuthorSchema).min(3));
const documents = world.generate(z.array(DocumentSchema).min(10));
const sentences = world.generate(z.array(SentenceSchema).min(30));
const annotations = world.generate(z.array(AnnotationSchema).min(50));
// annotations[*].sentenceId ∈ sentences[*].id ✓
// sentences[*].documentId ∈ documents[*].id ✓
// documents[*].authorId ∈ authors[*].authorId ✓Multi-API entity with several file types
One entity (person) owns multiple types of files (text, audio, bank). Each file type has its own API schema. A separate "entity API" aggregates all file IDs per person.
import { z } from "zod";
import { createWorld } from "zod4-mock";
const PersonSchema = z.object({ personId: z.uuid(), firstName: z.string(), lastName: z.string() });
const TextFileSchema = z.object({
fileId: z.uuid(),
ownerId: z.uuid(),
language: z.enum(["nl", "en", "de"]),
});
const AudioFileSchema = z.object({
fileId: z.uuid(),
ownerId: z.uuid(),
durationS: z.number().int().min(1),
});
const RawDataSchema = z.object({
id: z.uuid(),
type: z.enum(["text", "audio"]),
uploadedAt: z.date(),
});
const EntityApiSchema = z.object({
personId: z.uuid(),
firstName: z.string(),
lastName: z.string(),
fileIds: z.array(z.uuid()),
fileCount: z.number().int().min(0),
});
function createMediaWorld(seed = 42) {
return (
createWorld({ seed })
.withSchema(PersonSchema)
.withSchema(TextFileSchema, {
relations: { owner: PersonSchema },
matchers: { ownerId: (ctx) => ctx.related("owner").personId },
})
.withSchema(AudioFileSchema, {
relations: { owner: PersonSchema },
matchers: {
ownerId: (ctx) => ctx.related("owner").personId,
durationS: (ctx) => ctx.prng.int(30, 3600),
},
})
// Same output schema, two source schemas — type discriminator per binding
.withSchema(RawDataSchema, {
from: TextFileSchema,
matchers: { id: (ctx) => ctx.source.fileId, type: () => "text" as const },
})
.withSchema(RawDataSchema, {
from: AudioFileSchema,
matchers: { id: (ctx) => ctx.source.fileId, type: () => "audio" as const },
})
// Entity API aggregates file IDs across all file types
.withSchema(EntityApiSchema, {
from: PersonSchema,
matchers: {
personId: (ctx) => ctx.source.personId,
firstName: (ctx) => ctx.source.firstName,
lastName: (ctx) => ctx.source.lastName,
fileIds: (ctx) =>
[
...ctx.registry.filter(TextFileSchema, (f) => f.ownerId === ctx.source.personId),
...ctx.registry.filter(AudioFileSchema, (f) => f.ownerId === ctx.source.personId),
].map((f) => f.fileId),
fileCount: (ctx) =>
ctx.registry.filter(TextFileSchema, (f) => f.ownerId === ctx.source.personId).length +
ctx.registry.filter(AudioFileSchema, (f) => f.ownerId === ctx.source.personId).length,
},
})
);
}
const world = createMediaWorld(42).populate(PersonSchema, 3);
const rawdata = world.generate(z.array(RawDataSchema).min(10));
const entities = world.generate(z.array(EntityApiSchema));
// rawdata[*].id appears in exactly one entity's fileIds ✓Force a specific field value
const failed = world.generate(FileSchema, { overrides: { status: "failed" } });
// Nested objects deep-merge
const locked = world.generate(UserSchema, {
overrides: { role: "viewer", settings: { notifications: false } },
});Arrays in overrides replace rather than merge:
world.generate(InvoiceSchema, { overrides: { lines: [] } });Fix one item in an array
Use transform for array-index edits — overrides replaces arrays entirely:
const invoice = world.generate(InvoiceSchema, {
transform: (data) => ({
...data,
lines: data.lines.map((line, i) => (i === 0 ? { ...line, quantity: 99 } : line)),
}),
});Control optional field probability
By default, z.optional() / z.nullable() fields are omitted 20% of the
time. Fix this globally or per-call:
// Always generate optional fields (good for most test suites)
const world = createWorld({ seed: 42, optionalProbability: 0 });
// Pin a specific optional field in one generate call
const user = world.generate(UserSchema, { overrides: { middleName: "Maria" } });Opt out of realistic numeric distributions
Money / scale-free measurement keys default to log-uniform (Benford-conforming)
and shaped keys (age, year, quantity, count)
default to their real-world distributions. To replace any of these with a uniform draw (or any
other custom distribution), register a per-key generator via withGenerators:
import { createWorld } from "zod4-mock";
import { resolveNumberBounds } from "zod4-mock/internal"; // optional bounds helper
const world = createWorld({ seed: 42 }).withGenerators({
// Force uniform `amount` (faker-style) instead of Benford-conforming log-uniform.
amount: (schema, ctx) => {
const { min, max } = resolveNumberBounds(schema, 1, 10000);
return parseFloat((ctx.prng.random() * (max - min) + min).toFixed(2));
},
// Force uniform-int `quantity` instead of truncated geometric.
quantity: (schema, ctx) => ctx.prng.int(1, 100),
});withGenerators overrides win over the built-in key heuristics — see the pipeline
order in Concepts.
Derive one schema from another
When two API shapes represent the same entity, bind one to the other with from.
The source entity's data is available as ctx.source, so the derived record stays
consistent with its source:
const world = createWorld({ seed: 42 })
.withSchema(PersonSchema)
.withSchema(PersonSummarySchema, {
from: PersonSchema,
matchers: {
id: (ctx) => ctx.source.personId,
displayName: (ctx) => `${ctx.source.firstName} ${ctx.source.lastName}`,
},
});
const people = world.generate(z.array(PersonSchema).min(5));
const summaries = world.generate(z.array(PersonSummarySchema));
// people[0].personId === summaries[0].id — alwaysLocalize the output
By default, generators draw from a minimal built-in English locale — short
curated name/word lists, no Markov generation. For realistic, Markov-generated data or a
different language, install a locale package and pass it via locale:
npm install @zod4-mock/locale-en # rich English
npm install @zod4-mock/locale-nl # Dutch (Markov names, € prices, tussenvoegsels)import { createWorld } from "zod4-mock";
import { en } from "@zod4-mock/locale-en";
import { nl } from "@zod4-mock/locale-nl";
const enWorld = createWorld({ seed: 42, locale: en });
const nlWorld = createWorld({ seed: 42, locale: nl });Locales are plain objects implementing the LocaleData interface — every section
(names, words, currencies, addresses, phone formats, …) is overridable. For variants like
British English or nl-BE, use the extend() helper:
import { createWorld } from "zod4-mock";
import { en, extend } from "@zod4-mock/locale-en";
const enGB = extend(en, {
address: { ...en.address, phonePrefix: "+44", countryCode: "GB", ibanPrefix: "GB" },
commerce: { ...en.commerce, formatPrice: (n) => `£${n.toFixed(2)}` },
});
createWorld({ seed: 1, locale: enGB });See Localization in the API reference for the full interface.