Skip to content
Logo

faker

fabricator generates values, not human-plausible-looking ones. @faker-js/faker generates plausible ones, but from its own separate randomness — so a schema mixing the two has two seeds to track and only one of them replays.

The extension removes the second seed. Faker's generators draw from the leaf's own seeded stream, so T.faker.person.fullName() replays exactly like T.string does.

import { en } from "@faker-js/faker";
import { initialize, registry } from "@ghostry/fabricator";
import { fakerExtension } from "@ghostry/fabricator-extension-faker-v10";
 
const { T, Fabricator } = initialize({
  seed: "checkout",
  types: registry.extend(fakerExtension({ locale: en })),
});
 
const User = T.object({
  name: T.faker.person.fullName(),
  email: T.faker.internet.email(),
  joined: T.faker.date.past(),
  id: T.string.whereby({ length: { max: 8 } }),
});
 
new Fabricator(User).fabricate();

It arrives through registry.extend — the same augmentation path any user-defined type takes — so T.faker.* sits alongside every core kind rather than replacing anything.

locale takes faker's own locale definitions rather than a locale name, exactly as new Faker({ locale }) does; importing only the ones you use keeps the rest out of your bundle.

Every builder is a real kind, never opaque

The straightforward way to bridge a faker method is T.opaque(() => faker.person.fullName()), and that is precisely what this package refuses to do. An opaque schema is a value the library can say nothing about: it converts to Type.Unknown() in any adapter, and combinatorial/coverage can only treat it as a single outcome.

Each builder instead returns the core kind matching the method's own declared return type:

faker returnsbuilder returnsvia the TypeBox adapter
string (207)T.stringType.String()
Date (7)T.dateType.Date()
a record shape (7)T.object({ ... })Type.Object({ ... })
a literal union (6)T.enum.uniform([...])Type.Union([Type.Literal(), …])
number (6)T.numberType.Number()
boolean (1)T.booleanType.Boolean()
bigint (1)T.bigintType.BigInt()
number[] (1)T.array(T.number)Type.Array(Type.Number())
Date[] (1)T.array(T.date)Type.Array(Type.Date())

So a faker-populated schema validates, enumerates, and converts exactly like a hand-written one:

import { toTypeBox } from "@ghostry/fabricator-adapter-typebox-v0";
 
toTypeBox(T.faker.person.sexType());
// Type.Union([Type.Literal('female'), Type.Literal('generic'), Type.Literal('male')])

Seventeen string methods whose output satisfies a JSON-Schema format regardless of which options you pass additionally carry that hint, so it forwards into the converted schema:

toTypeBox(T.faker.internet.email()); // Type.String({ format: 'email' })
toTypeBox(T.faker.database.mongodbObjectId()); // Type.String({ pattern: '^[0-9a-fA-F]{24}$' })

The list is curated rather than exhaustive: a method whose shape varies with its options — git.commitSha's length, say — is left unhinted rather than hinted with a caveat.

Keeping the mapping honest

The kind for each method is a committed table, generated by constructing a Faker and classifying what each method actually returns. A generated table can drift from the library it was generated against, so the enforcement is at the type level rather than in the generator: a compile-time assertion compares every entry's resolved value type against faker's own declared return type.

A faker release that changes a return type therefore fails the type check at that exact entry, instead of silently producing a schema that disagrees with the data.

Modules

Twenty-six data modules are mirrored, with faker's own method names beneath each:

airline, animal, book, color, commerce, company, database, datatype, date, finance, food, git, hacker, image, internet, location, lorem, music, number, person, phone, science, string, system, vehicle, word

Each builder takes the same options its faker method does, so what you already know transfers:

T.faker.lorem.paragraphs({ min: 2, max: 4 });
T.faker.number.int({ min: 1, max: 100 });
T.faker.date.past({ years: 3 });

Where a faker module overlaps something core already does — datatype.boolean, number.int, string.alphanumeric — both remain available. Faker's are locale-aware and shaped like its own API; core's are configurable and enumerable. Which to reach for is a genuine choice, not a redundancy to resolve.

Three deviations from faker's own API

The mirror is deliberately not 1:1. Each departure is what makes the guarantee above possible, and each is somewhere a faker-literate reader will otherwise be surprised.

helpers is absent

It is faker's utility belt, not a data module, and core already expresses all of it — usually better, since core's versions are enumerable and adapter-visible:

faker.helpers.…core
arrayElement, objectKey, objectValue, enumValueT.enum.uniform([...])
weightedArrayElementT.enum.weighted([...])
arrayElements, multipleT.array(...).whereby({ length })
maybeT.optional / T.omittable / T.undefinable
rangeToNumberT.number.whereby({ min, max })

Eleven of its eighteen methods are generic, so mirroring them would erase to unknown anyway. slugify, mustache, and replaceSymbols are transforms over your own input rather than generators at all. The two with no core equivalent — fromRegExp and fake — are reached through use.

Seven color.* methods are split in two

color.rgb() returns a string or a number[] depending on options.format, with different defaults per method. No single kind can honestly describe that, and inspecting the arguments at call time would make the schema's type depend on a value. Each becomes a namespace of two named builders instead:

T.faker.color.rgb.text(); // T.string
T.faker.color.rgb.text({ format: "css" });
T.faker.color.rgb.channels(); // T.array(T.number)
T.faker.color.rgb.channels({ includeAlpha: true });

There is deliberately no bare T.faker.color.rgb() — every form is named. The same applies to cmyk, hsl, hwb, lab, lch, and colorByCSSColorSpace; color's other four methods are ordinary builders.

Channels are T.array(T.number) rather than T.tuple because the arity is option-dependent: includeAlpha adds one, and cmyk has four where the rest have three. A fixed-arity tuple would be wrong for exactly the arguments most likely to be passed.

A literal union becomes an enum, not a string

Where faker declares a narrower return type than its JS type, the builder narrows with it:

T.faker.person.sexType(); // T.enum.uniform(['female', 'generic', 'male'])

So the field is enumerable by combinatorial/coverage and converts to a union of literals, rather than an unconstrained string that happens to hold one of three values.

use — for what the mirror doesn't cover

use hands you the shared, stream-backed Faker inside a producer, so anything reached through it still draws from that leaf's own seeded stream. Every form is kind-tagged: you say what shape comes back, and keep a real kind.

T.faker.use.string((f) => f.helpers.fromRegExp("[A-Z]{3}-[0-9]{4}"));
T.faker.use.string((f) =>
  f.helpers.fake("{{person.firstName}} {{person.lastName}}"),
);
T.faker.use.number((f) => f.helpers.rangeToNumber({ min: 1, max: 10 }));
T.faker.use.opaque((f) => f.helpers.arrayElement(["free", "pro"] as const));

use.string, .number, .date, .boolean, and .bigint stay adapter-compatible. use.opaque is the only route to an opaque schema in this package — which is honest, because it is the one case where you have told it nothing about the shape.

What "now" means

Faker's relative-date methods resolve against the same instance clock T.date.past/T.date.future do, so the two never disagree within one schema:

T.object({ core: T.date.past, faker: T.faker.date.past() });

That includes a wrap({ clock }, …) in effect at fabricate time — the clock is read per fabrication, not captured when the extension was built. See Reproducibility for what the clock is and how it defaults.

There is deliberately no reference-date option on fakerExtension. A second, independently-configured clock would be a second source of truth, free to drift from T.date.past in the same object.

Reproducibility

Everything on Reproducibility applies unchanged: the same seed and clock produce the same faker output, and a field's stream is keyed by its structural position, so inserting or reordering a sibling leaves the others untouched.

Two consequences worth stating outright:

faker.seed(...) is inert. Seeding is fabricator's job here, and a second seed competing for control of the same output is the problem this package exists to remove. Change initialize({ seed }) instead.

The global faker singleton is untouched. The extension builds its own Faker on its own randomizer, so seeding or draining the singleton elsewhere in your test suite cannot perturb fabricated output — and vice versa.

If you need a Faker built differently, create receives the bridge's randomizer and hands back the instance to use, so reproducibility survives:

fakerExtension({
  create: (randomizer) => new Faker({ locale: [de, en], randomizer }),
});

A pre-built Faker cannot be passed instead: its randomizer is fixed at construction, so it has no way to reach fabricator's stream.

Errors

A builder called outside fabricate() throws FakerExtensionError.NoActiveScopeError — there is no active fabrication whose stream it could draw from. This is what catches a builder captured out of a schema and invoked directly.

FakerExtensionError extends core's FabricatorError, so a single catch still covers both packages:

import { FabricatorError } from "@ghostry/fabricator";
 
try {
  /* ... */
} catch (e) {
  if (e instanceof FabricatorError) {
    /* both packages' failures land here */
  }
}