by M2Toolkit · v0.1.0 · early release

Test data you can
replay exactly.

Deterministic, schema-aware, relational fixtures and edge cases for TypeScript and JavaScript. Zero runtime dependencies.

npm install --save-dev seedset

Why Seedset

A fixture engine,
not another Faker.

Learn more

Describe your records once and get the same data on every machine: typed factories, relational datasets whose foreign keys resolve, and labelled boundary and invalid cases for any schema.

Each (record, field) has its own random stream, so adding a field never churns your existing snapshots. Use Faker inside a field function when you need a richer vocabulary; Seedset handles the structure. Framework-agnostic: plain functions with no adapter, and zero dependencies.

ts
import { createSeed, t } from "seedset";

const seed = createSeed({ seed: 42 });

const userFactory = seed.factory(
  {
    id: (_g, ctx) => ctx.sequence,
    name: (g) => g.person.fullName(),
    email: (g) => g.internet.email(),
    age: t.int({ min: 18, max: 80 }),
    role: "member" as string,
  },
  { name: "user", traits: { admin: { role: "admin" } } },
);

const users = seed.generate(userFactory, 100); // same 100 users every run
const alex = seed.build(userFactory, { name: "Alex Morgan", age: 32 }); // typed overrides

Seeded. Relational. Verified.

Reproduce a failure exactly

Same seed, same factory, same output. Every error and report carries the seed, and unseeded runs pick one and expose it as seed.seed. generate(f, 10) is always a prefix of generate(f, 100).

ts
import { createSeed } from "seedset";

const a = createSeed({ seed: "checkout" });
const b = createSeed({ seed: "checkout" });
const person = (s: typeof a) => s.factory({ name: (g) => g.person.fullName() }, { name: "person" });

// Same seed, same data. A prefix of a bigger run, too:
a.generate(person(a), 10); // identical to the first ten of
b.generate(person(b), 100);

// Unseeded runs pick a random seed and tell you what it was:
const run = createSeed();
console.log(`seed: ${run.seed}`); // SEED=2008428232 npm test

Relations that resolve

dataset() generates users, orders and items with real foreign keys: cardinality, uniqueness, distributions like zipf, and cycle detection that names the cycle.

ts
import { createSeed } from "seedset";

const seed = createSeed({ seed: "shop" });
const id = (_g: unknown, c: { sequence: number }) => c.sequence;
const user = seed.factory({ id, name: (g) => g.person.fullName() }, { name: "user" });
const order = seed.factory({ id, status: (g) => g.helpers.pick(["new", "paid", "shipped"] as const) }, { name: "order" });

const data = seed.dataset({
  users: { factory: user, count: 10 },
  orders: {
    factory: order,
    count: 40,
    refs: { userId: { to: "users", distribution: "zipf" } },
  },
});
// every data.orders[i].userId is a real data.users[].id

Boundary and invalid inputs

edgeCases(schema) returns labelled cases at every bound, one beyond, plus wrong types and missing keys. Each case is re-checked and verified valid or invalid before it is returned.

ts
import { createSeed, t } from "seedset";

const seed = createSeed({ seed: 7 });
const signup = t.object({
  username: t.string({ minLength: 3, maxLength: 12 }),
  age: t.int({ min: 13, max: 120 }),
});

for (const c of seed.edgeCases(signup)) {
  // c.valid, c.category, c.label, c.path, c.constraint, c.value
  console.log(c.valid ? "accept" : "reject", c.label);
}
// accept string at maxLength   reject string above maxLength   reject integer below minimum ...

From Zod or JSON Schema

Zod → z.toJSONSchema → fromJsonSchema, and the outputs can be checked by the original validator. Unsupported keywords are rejected with a path, never silently ignored.

ts
import { z } from "zod";
import { createSeed, fromJsonSchema } from "seedset";

const User = z.object({ id: z.string().uuid(), age: z.number().int().min(18).max(80) });
const schema = fromJsonSchema(z.toJSONSchema(User, { io: "input" }));

const seed = createSeed({ seed: 1 });
const users = seed.generate(schema, 100);
users.every((u) => User.safeParse(u).success); // true
  • Zod 4
  • JSON Schema
  • Standard Schema

Millions of rows, flat memory

stream(), batches() and batchesAsync() yield exactly the records generate() would, one batch at a time, with abort between batches.

ts
import { createSeed } from "seedset";

const seed = createSeed({ seed: "load-test" });
const userFactory = seed.factory({ id: (_g, c) => c.sequence, name: (g) => g.person.fullName() }, { name: "user" });

for (const batch of seed.batches(userFactory, { count: 1_000_000, batchSize: 5000 })) {
  insertMany(batch); // memory stays flat: one batch at a time
}
declare function insertMany(rows: unknown[]): void;

Errors that explain themselves

Failures give the path, constraint, seed and a hint, and never print the generated value. Typed overrides are compile errors, and schema-backed fields reject bad overrides at runtime.

ts
import { SeedsetError, createSeed, t } from "seedset";

const seed = createSeed({ seed: 42 });
const userFactory = seed.factory({ age: t.int({ min: 18, max: 80 }) }, { name: "user" });

try {
  seed.build(userFactory, { age: 5 });
} catch (e) {
  if (e instanceof SeedsetError) console.error(e.message, e.path, e.constraint, e.seed, e.hint);
}
// OverrideError: Override for "age" violates the field's schema:
//   value is below minimum 18 (path: age, constraint: min, seed: 42)

Playground

Try it live. Change the seed, watch the data.

Every result below is generated in your browser by the same Seedset source this page documents. Nothing is mocked.

Codeusers.ts
ts
import { t, type Seed } from "seedset";

/** Ten reproducible users. Change the seed and the whole set changes; keep it and nothing does. */
export function users(seed: Seed, count: number) {
  const userFactory = seed.factory(
    {
      id: (_g, ctx) => ctx.sequence,
      name: (g) => g.person.fullName(),
      email: (g) => g.internet.email(),
      age: t.int({ min: 18, max: 80 }),
      role: (g) => g.helpers.weighted([{ value: "member", weight: 8 }, { value: "admin", weight: 1 }]),
    },
    { name: "user" },
  );
  return seed.generate(userFactory, count);
}
Options
✓ Generated
[
  {
    "id": 1,
    "name": "Keiko Vargas",
    "email": "hugo_5344@example.com",
    "age": 50,
    "role": "member"
  },
  {
    "id": 2,
    "name": "Bruno Silva",
    "email": "hugo_1359@example.org",
    "age": 24,
    "role": "member"
  },
  {
    "id": 3,
    "name": "Ivan Weber",
    "email": "elena_872@example.com",
    "age": 19,
    "role": "member"
  },
  {
    "id": 4,
    "name": "Felix Reyes",
    "email": "ben_2607@example.com",
    "age": 23,
    "role": "member"
  },
  {
    "id": 5,
    "name": "Jonas Ivanov",
    "email": "amara_3458@example.org",
    "age": 43,
    "role": "member"
  }
]

Change the seed and every user changes. Set it back and the same users return. Raise the count: the first five never move. Read more →

0Runtime dependencies
~18 kBmin+gzip, the whole library
173small synthetic vocabulary entries
ESM + CJSNode.js 20+, types included

Comparison

Where it fits next to the tools you already use.

Competitors are better at what they target. Use them together, for example Faker values inside a Seedset field. Based on the docs reviewed on 2026-10-27; see the evidence and gaps.

CapabilitySeedsetFakerFisheryjson-schema-faker
Large multi-locale value libraryno (173 entries)yesnooptional via Faker
Typed factoriesyesnoyesno
Per-field stream isolationyesnot documentedn/anot documented
Relational datasets with FK checksyesnoassociations onlyno
Labelled boundary / invalid fixturesyesnononot found
Streaming generationyesnonono
JSON Schema coveragesmall subset––broad
Runtime dependencies0000

Documentation

From your first factory to million-row streams.

Make your tests repeatable with Seedset

One seed in, the same data out. Every failure comes with the seed that reproduces it.