npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zipbul/baker

v3.4.1

Published

Bun-only AOT decorator-based DTO validation & serialization. class-validator DX, sealed code generation, zero reflect-metadata.

Readme

@zipbul/baker

The fastest decorator-based DTO validation library for TypeScript. Generates optimized validation and serialization code on first seal, then reuses the sealed executors on every call.

bun add @zipbul/baker

Zero reflect-metadata. Sealed codegen. 99%+ line coverage.

Requires Bun ≥ 1.3.13. baker relies on TC39 decorator metadata (Symbol.metadata), which Node does not populate — it is Bun-only.

Quick Start

import { deserialize, isBakerIssueSet, Field, Recipe, seal } from '@zipbul/baker';
import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';

@Recipe
class UserDto {
  @Field(isString, minLength(2)) name!: string;
  @Field(isNumber(), min(0)) age!: number;
  @Field(isString, isEmail()) email!: string;
}

// Call once at app startup, after all DTOs are loaded.
seal();

const result = await deserialize(UserDto, {
  name: 'Alice',
  age: 30,
  email: '[email protected]',
});

if (isBakerIssueSet(result)) {
  console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
} else {
  console.log(result.name); // 'Alice' — typed as UserDto
}

Why Baker?

Baker generates optimized JavaScript functions once on first seal, then executes them on every call.

| Feature | baker | class-validator | Zod | | ----------------------- | -------------------- | ---------------------- | ------------------- | | Valid path (5 fields) | fast sealed path | slower | slower | | Invalid path (5 fields) | fast sealed path | slower | slower | | Approach | AOT code generation | Runtime interpretation | Schema method chain | | Decorators | @Field (unified) | 30+ individual | N/A | | reflect-metadata | Not needed | Required | N/A | | Sync DTO return | Direct value | Promise | Direct value |

Performance

Benchmarked against multiple libraries on simple, nested, array, and error-collection scenarios. Exact numbers vary by machine and runtime.

See bench/ for the current benchmark suite and exact scenarios.

API

seal(...classes?)

Required. Call once at app startup, after every DTO module has been imported. With no arguments, seals every class registered via @Field so far. With class arguments, seals only those (and any nested DTOs they reach). Idempotent.

deserialize / serialize / validate throw BakerError if the DTO is not sealed. Tests that need to mutate decorator metadata should call seal() after each configure(...) reconfiguration.

deserialize<T>(Class, input, options?)

Returns T | BakerIssueSet for sync DTOs, Promise<T | BakerIssueSet> for async DTOs. Never throws on validation failure.

If the DTO has any async rule or transformer, deserialize returns a Promise. Otherwise it returns the value directly. For full type safety pick a strict variant (see below).

deserializeSync<T> / deserializeAsync<T>

Strict variants. deserializeSync throws BakerError if the DTO is async on the deserialize side. deserializeAsync always returns Promise (sync DTOs are wrapped via Promise.resolve).

serialize<T>(instance, options?)

Returns Record<string, unknown> for sync DTOs, Promise<Record<string, unknown>> for async DTOs. No validation. Async asymmetry: _isSerializeAsync is independent of _isAsync — a DTO can be async on deserialize but sync on serialize, and vice versa.

serializeSync<T> / serializeAsync<T>

Strict variants. serializeSync throws BakerError if the DTO is async on the serialize side.

validate(Class, input, options?)

Validates input against a decorated class's schema. Returns true | BakerIssueSet for sync paths, Promise<true | BakerIssueSet> for async paths. To validate a single primitive without a DTO, call the rule directly (e.g. isEmail()(value)).

validateSync / validateAsync

Strict variants. validateSync throws BakerError if the DTO is async; validateAsync always returns Promise.

isBakerIssueSet(value)

Type guard. Narrows result to BakerIssueSet containing { path, code, message?, context? }[].

configure(config)

Global configuration. Must be called before seal(). After seal, configure(...) throws BakerError; reconfiguring requires unseal() (test-only helper) + configure(...) + seal() again.

configure({
  autoConvert: true, // coerce "123" → 123
  allowClassDefaults: true, // use class field initializers for missing keys
  stopAtFirstError: true, // return on first validation failure
  forbidUnknown: true, // reject undeclared fields
});

createRule(name, validate) / createRule(options)

Custom validation rule. Two forms — a (name, validate) shorthand or an options object:

const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
const isEven = createRule({
  name: 'isEven',
  validate: v => typeof v === 'number' && v % 2 === 0,
  requiresType: 'number',
});

@Field Decorator

One decorator for everything — replaces 30+ individual decorators from class-validator.

Only fields decorated with @Field participate in validation, deserialization, and serialization. Undecorated fields are silently absent from results — they are not part of the DTO contract.

@Field(...rules)
@Field(...rules, options)
@Field(options)
@Field()                    // marker-only (no rules)

Each rule must be an emittable rule object created via createRule() or one of the built-in rule factories. Passing a raw function (e.g. @Field(isNumber) instead of @Field(isNumber())) throws BakerError at decorator-evaluation time.

Options

| Option | Type | Description | | ----------------- | ------------------------------------------------- | ------------------------------ | | type | () => Dto \| [Dto] | Nested DTO. [Dto] for arrays | | discriminator | { property, subTypes } | Polymorphic dispatch | | keepDiscriminatorProperty | boolean | Keep the discriminator key in the result | | optional | boolean | Allow undefined | | nullable | boolean | Allow null | | name | string | Bidirectional key mapping | | deserializeName | string | Input key mapping | | serializeName | string | Output key mapping | | exclude | boolean \| 'deserializeOnly' \| 'serializeOnly' | Field exclusion | | groups | string[] | Conditional visibility | | when | (obj) => boolean | Conditional validation | | transform | Transformer \| Transformer[] | Value transformer | | message | string \| (args) => string | Error message override | | context | unknown | Error context | | mapValue | () => Dto | Map value DTO | | setValue | () => Dto | Set element DTO |

Transformers

Bidirectional value transformers with separate deserialize and serialize methods.

import type { Transformer } from '@zipbul/baker';

const centsTransformer: Transformer = {
  deserialize: ({ value }) => (typeof value === 'number' ? value * 100 : value),
  serialize: ({ value }) => (typeof value === 'number' ? value / 100 : value),
};

Built-in Transformers

import {
  trimTransformer,
  toLowerCaseTransformer,
  toUpperCaseTransformer,
  roundTransformer,
  unixSecondsTransformer,
  unixMillisTransformer,
  isoStringTransformer,
  csvTransformer,
  jsonTransformer,
} from '@zipbul/baker/transformers';

| Transformer | deserialize | serialize | | ------------------------ | -------------------------- | -------------------------- | | trimTransformer | trim string | trim string | | toLowerCaseTransformer | lowercase | lowercase | | toUpperCaseTransformer | uppercase | uppercase | | roundTransformer(n?) | round to n decimals | round to n decimals | | unixSecondsTransformer | unix seconds → Date | Date → unix seconds | | unixMillisTransformer | unix ms → Date | Date → unix ms | | isoStringTransformer | ISO string → Date | Date → ISO string | | csvTransformer(sep?) | "a,b"["a","b"] | ["a","b"]"a,b" | | jsonTransformer | JSON string → object | object → JSON string |

Transform Array Order

Multiple transformers apply as a codec stack:

  • Deserialize: left to right — [A, B, C] applies A, then B, then C
  • Serialize: right to left — [A, B, C] applies C, then B, then A
@Field(isString, { transform: [trimTransformer, toLowerCaseTransformer] })
email!: string;
// deserialize "  HELLO  " → trim → toLowerCase → "hello"
// serialize   "hello"     → toLowerCase → trim → "hello"

Optional Peer Transformers

// bun add luxon
import { luxonTransformer } from '@zipbul/baker/transformers';
const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });

@Recipe
class EventDto {
  @Field({ transform: luxon }) startAt!: DateTime;
}
// bun add moment
import { momentTransformer } from '@zipbul/baker/transformers';
const mt = await momentTransformer({ format: 'YYYY-MM-DD' });

Note on format: The format option in luxonTransformer / momentTransformer controls the serialize-side output only. On deserialize, both transformers parse the input with the library's default parser (ISO-first for Luxon, lenient parser for Moment). Using a lossy format like 'YYYY-MM-DD' makes the transformer one-way — serialize → deserialize will not recover the original time of day. If you need a lossless roundtrip, omit format (defaults to ISO 8601).

Rules

105 built-in validation rules.

Type Checkers

isString, isInt, isBoolean, isDate, isArray, isObject — constants, no () needed.

isNumber(options?), isEnum(entity) — factories, require ().

Numbers

min(n), max(n), isPositive, isNegative, isDivisibleBy(n)

Strings

minLength(n), maxLength(n), length(min, max), contains(seed), notContains(seed), matches(regex)

Formats

isEmail(), isURL(), isUUID(version?), isIP(version?), isISO8601(), isJSON, isJWT, isCreditCard, isIBAN(), isFQDN(), isMACAddress(), isBase64(), isHexColor, isSemVer, isMongoId, isPhoneNumber(), isStrongPassword(), isULID(), isCUID2(), isHttpToken

Arrays

arrayMinSize(n), arrayMaxSize(n), arrayUnique(), arrayNotEmpty, arrayContains(values), arrayOf(...rules)

Common

equals(val), notEquals(val), isIn(values), isNotIn(values), isEmpty, isNotEmpty

Date

minDate(date), maxDate(date)

Locale

isMobilePhone(locale), isPostalCode(locale), isIdentityCard(locale), isPassportNumber(locale)

Nested DTOs

@Recipe
class AddressDto {
  @Field(isString) city!: string;
}

@Recipe
class UserDto {
  @Field({ type: () => AddressDto }) address!: AddressDto;
  @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
}

Collections

@Recipe
class UserDto {
  @Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
  @Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
}

Deserialize input shape: a Set field accepts a JSON array, a Map field accepts a plain object keyed by string. Serialize emits the same shapes.

Discriminator

@Recipe
class PetOwner {
  @Field({
    type: () => CatDto,
    discriminator: {
      property: 'kind',
      subTypes: [
        { value: CatDto, name: 'cat' },
        { value: DogDto, name: 'dog' },
      ],
    },
  })
  pet!: CatDto | DogDto;
}

Inheritance

@Recipe
class BaseDto {
  @Field(isString) id!: string;
}

@Recipe
class UserDto extends BaseDto {
  @Field(isString) name!: string;
  // inherits 'id' field with isString rule
}

FAQ

When should I use baker instead of class-validator?

When performance matters. baker generates optimized validation/serialization code at seal time instead of interpreting rules on every call, so it is substantially faster than class-validator on both valid and invalid input while providing the same decorator-based DX. baker also eliminates the reflect-metadata dependency. Run bench/ to measure the exact difference on your machine.

How does baker compare to Zod?

Zod uses schema method chains (z.string().email()), baker uses decorators (@Field(isString, isEmail())). baker generates optimized code at definition time instead of interpreting schemas at runtime. Choose Zod if you need schema-first design or Node support; choose baker if you need class-based DTOs on Bun with maximum performance.

Does baker support async validation?

Yes. If any rule or transformer is async, baker automatically detects it at seal time and generates an async executor. Sync DTOs return values directly without Promise wrapping.

Can I use baker with NestJS?

Yes. baker's @Field decorator works alongside NestJS pipes. Use deserialize() in a custom validation pipe.

How does the AOT code generation work?

Calling seal() once at app startup walks every registered DTO, analyzes field metadata, generates optimized JavaScript validation functions via new Function(), and caches them. Subsequent deserialize/serialize/validate calls execute the pre-compiled functions directly. There is no auto-seal — forgetting to call seal() raises BakerError on first use.

Exports

import {
  seal,
  deserialize, deserializeSync, deserializeAsync,
  validate,    validateSync,    validateAsync,
  serialize,   serializeSync,   serializeAsync,
  configure, createRule, Field, arrayOf, isBakerIssueSet, BakerError,
} from '@zipbul/baker';
import type { Transformer, TransformParams, BakerError, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
import { isString, isEmail, isULID, isCUID2, ... } from '@zipbul/baker/rules';
import { trimTransformer, jsonTransformer, ... } from '@zipbul/baker/transformers';

License

MIT