Schema — API Reference
Part of storymock. Import from
'storymock'or'storymock/schema'.
A schema is a typed factory for generating mock objects. It maps each field of a TypeScript interface to a faker, literal value, or conditional helper, and produces instances via .create(). For seeding, see Configuration — Seeding.
Table of Contents
- Schema Definition
- Field Types
- Traits
- Customization with
.with() - Conditional Logic:
when() - Computed Fields:
derive() - Dependency Resolution
- Schema Metadata
- Setup Callbacks:
.setup() - Batch Generation
1. Schema Definition
import { schema, numeric, text, temporal, person, choice, collection, lorem } from 'storymock';
interface User {
id: string;
name: string;
age: number;
birthdate: Date;
status: 'active' | 'inactive';
tags: string[];
}
const UserSchema = schema<User>({
id: text().uuid(),
name: person().fullName(),
age: person().age().min(18).max(80),
birthdate: temporal().past(50, 'years'),
status: choice('active', 'inactive'),
tags: collection(lorem().word()).maxLength(3),
});
const user: User = UserSchema.create();
// { id: 'a1c4e8b2-...', name: 'Sofia Reyes', age: 37, status: 'active', ... }The generic parameter <User> enables compile-time validation: every key of User must be present, and each field's faker must produce the correct type.
Throws
ContradictoryConstraintError— thrown at.create()time when field constraints conflict (e.g.numeric().min(10).max(5)). See errors for details.
2. Field Types
A schema field can be any of the following:
| Type | Description | Example |
|---|---|---|
Faker<T> | A faker that produces the field's type | text().uuid() |
Schema<T> | A nested schema (creates a sub-object) | AddressSchema |
Literal T | A constant value, used as-is every time | 'active' |
When<T> | Conditional based on a sibling field | when('type', { ... }) |
Derive<T> | Computed from resolved sibling fields | derive(({ a, b }) => ...) |
type SchemaDefinition<T> = {
[K in keyof T]:
| Faker<T[K]>
| Schema<T[K]>
| T[K]
| When<T, T[K]>
| Derive<T, T[K]>;
};Nested schemas are fully supported:
interface Address {
street: string;
city: string;
zip: string;
}
interface User {
name: string;
address: Address;
}
const AddressSchema = schema<Address>({
street: location().streetAddress(),
city: location().city(),
zip: location().zipCode(),
});
const UserSchema = schema<User>({
name: person().fullName(),
address: AddressSchema, // nested schema — creates a full Address
});3. Traits
Traits are named, reusable sets of field overrides. They represent a specific state of the mock data.
3.1 Defining Traits
const UserSchema = schema<User>({ /* base definition */ })
.trait('birthday', {
birthdate: temporal().ago(numeric().min(18).max(80), 'years'),
})
.trait('inactive', {
status: 'inactive' as const,
})
.trait('admin', {
tags: ['admin', 'staff'],
})
.trait('young', {
age: numeric().min(18).max(25),
birthdate: temporal().ago(numeric().min(18).max(25), 'years'),
});3.2 Trait Rules
- A trait is a partial schema definition — it only needs to include the fields it overrides
- Trait fields are type-checked against the original interface
- Trait values follow the same rules as schema fields (fakers, literals, when, derive)
- Defining a trait returns a new schema (immutable)
// ❌ Type error — 'email' is not a field of User
UserSchema.trait('invalid', { email: text() });
// ❌ Type error — age must be number, not string
UserSchema.trait('invalid', { age: text() });Throws
InvalidTraitError— thrown when applying a trait name that was never defined on the schema. See errors for details.
4. Customization with .with()
.with() is the single method for applying traits and field overrides. There is no separate .override() method.
4.1 Applying Traits
const birthdayUser = UserSchema.with('birthday').create();
// { id: '4f8a1c3d-...', name: 'Kenji Watanabe', age: 62, status: 'active', ... }
const inactiveAdmin = UserSchema.with('inactive', 'admin').create();
// { id: 'd2b7e5f0-...', name: 'Amara Osei', age: 29, status: 'inactive', tags: ['admin', 'staff'], ... }Multiple traits are applied left-to-right. Later traits override earlier ones for overlapping fields.
4.2 Applying Field Overrides
Pass an object as the last argument to override specific fields inline:
const customUser = UserSchema.with({ name: 'Eldar', age: 30 }).create();
// { id: '9c3f2a71-...', name: 'Eldar', age: 30, status: 'active', ... }4.3 Combining Traits and Overrides
const birthdayEldar = UserSchema.with('birthday', { name: 'Eldar' }).create();
// { id: 'e6d0b8a2-...', name: 'Eldar', age: 44, status: 'inactive', ... }
const youngAdmin = UserSchema.with('young', 'admin', { name: 'Alice' }).create();
// { id: '1a5c9f3e-...', name: 'Alice', age: 22, tags: ['admin', 'staff'], ... }Traits are applied first (left-to-right), then the field override object is applied last.
4.4 Immutability
.with() always returns a new schema. The original is unchanged:
const base = UserSchema;
const birthday = base.with('birthday'); // new schema
const admin = base.with('admin'); // another new schema, independent of birthdayFor .with() on stories, see Story API — .with().
5. Conditional Logic: when()
when() selects a faker based on the resolved value of a sibling field. It creates an implicit dependency edge in the resolution graph (see §7).
Signature
function when<T, K extends keyof T, R>(
field: K,
cases: Record<string, R | Faker<R>> & { _?: R | Faker<R> },
): When<T, R>;Rules
- Cases can be fakers or literal values
- Supports string, number, and boolean discriminants
_is the default/else case — used when no other key matches- The matched field must resolve before the dependent field
price: when('type', {
percentage: numeric().min(5).max(100),
fixed: numeric().min(10).max(200).precision(2),
}),
expiration: when('status', {
expired: temporal().past(1, 'years'),
_: temporal().future(1, 'years'), // default case
}),See Working with Schemas — Conditional Fields for full examples with string, numeric, and boolean matching.
Throws
MissingCaseError— thrown at.create()time when no case matches and no_default is provided. See Errors.
6. Computed Fields: derive()
derive() computes a field from resolved sibling values.
Signature
function derive<T, R>(
fn: (resolved: Partial<T>) => R | Faker<R>,
): Derive<T, R>;Rules
- The callback receives a
Partial<T>of all fields resolved so far - Can return a literal value or a faker (which will be
.create()'d automatically) derive()fields resolve after all non-derived fields
title: derive(({ type, price }) =>
type === 'percentage' ? `${price}% OFF` : `$${price} OFF`
),See Working with Schemas — Computed Fields for full examples.
7. Dependency Resolution
Fields form a directed acyclic graph (DAG) based on their dependencies:
- Definition time:
when()calls build the dependency graph - Definition time: Topological sort — throw
CircularDependencyErrorif a cycle exists - Create time: Resolve fields in topological order
- Create time: Resolve all
derive()fields last
| Field type | Resolution order | Dependencies |
|---|---|---|
| Plain faker / literal | First | None |
when() field | After its dependency | The matched sibling |
derive() field | Last | All non-derived fields |
See Working with Schemas — How Fields Resolve for a detailed walkthrough.
Throws
CircularDependencyError— thrown at definition time whenwhen()edges form a cycle. See Errors.
8. Schema Metadata
.id(fn)
Declares how to extract the identity of a generated instance. Used by ref() in stories to resolve foreign keys.
const UserSchema = schema<User>({ /* ... */ })
.id((user) => user.id);Resolution chain
When a story evaluates ref('user'), it resolves the identity through this chain (first match wins):
.id()accessor — if the schema has.id(fn), callsfn(instance)and uses the return valueidproperty — if the generated object has a property namedid, uses its value- String value — if the entire generated value is a string (e.g., from a bare
text()schema), uses it directly - Error — throws
UnknownRefError
When to define .id() explicitly
Most schemas have an id field, so ref() resolves automatically. Define .id() when:
- The primary key has a non-standard name (
_id,uuid,pk) - The identity is composite
- The identity requires transformation
// Non-standard key name
const DocSchema = schema<Doc>({ _id: text().objectId(), ... })
.id((doc) => doc._id);
// Composite identity
const EdgeSchema = schema<Edge>({ from: text().uuid(), to: text().uuid(), ... })
.id((edge) => `${edge.from}:${edge.to}`);See Stories — ref() for how resolution works at the story level.
9. Setup Callbacks: .setup()
.setup() records a callback — no mutation happens until .create() is called. At create time, all setup callbacks execute sequentially on the generated objects.
const UserSchema = schema<User>({
id: text().uuid(),
name: person().fullName(),
age: person().age().min(18).max(80),
})
.setup((user) => {
user.name = user.name.toUpperCase();
})
.setup((user) => {
console.log('Generated user:', user.id);
});
const user = UserSchema.create();
// 1. Object is generated from the schema definition
// 2. First .setup() callback runs — uppercases the name
// 3. Second .setup() callback runs — logs the id.setup() returns a new schema (immutable). Each callback receives the fully resolved object and can mutate it in place. Callbacks execute in registration order.
10. Batch Generation
Generate multiple instances:
const users: User[] = UserSchema.create(10);
// [{ id: 'a1c4...', name: 'Sofia Reyes', age: 37, ... }, ...] — 10 users
const admins: User[] = UserSchema.with('admin').create(5);
// [{ id: 'f7a2...', name: 'Kenji Watanabe', tags: ['admin', 'staff'], ... }, ...] — 5 adminsEach instance in the batch is independently generated (different random values, unless seeded).