@zipbul/baker 2.2.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +132 -69
  3. package/dist/index.d.ts +7 -6
  4. package/dist/index.js +10 -321
  5. package/dist/src/collect.d.ts +13 -10
  6. package/dist/src/collect.js +26 -0
  7. package/dist/src/configure.d.ts +8 -6
  8. package/dist/src/configure.js +43 -0
  9. package/dist/src/create-rule.js +41 -0
  10. package/dist/src/decorators/field.d.ts +22 -18
  11. package/dist/src/decorators/field.js +268 -0
  12. package/dist/src/decorators/index.d.ts +1 -0
  13. package/dist/src/decorators/index.js +2 -2
  14. package/dist/src/decorators/recipe.d.ts +17 -0
  15. package/dist/src/decorators/recipe.js +23 -0
  16. package/dist/src/errors.d.ts +27 -17
  17. package/dist/src/errors.js +52 -0
  18. package/dist/src/functions/check-call-options.d.ts +8 -0
  19. package/dist/src/functions/check-call-options.js +51 -0
  20. package/dist/src/functions/deserialize.d.ts +13 -6
  21. package/dist/src/functions/deserialize.js +57 -0
  22. package/dist/src/functions/serialize.d.ts +10 -4
  23. package/dist/src/functions/serialize.js +52 -0
  24. package/dist/src/functions/validate.d.ts +13 -10
  25. package/dist/src/functions/validate.js +49 -0
  26. package/dist/src/interfaces.d.ts +1 -1
  27. package/dist/src/interfaces.js +4 -0
  28. package/dist/src/meta-access.d.ts +19 -0
  29. package/dist/src/meta-access.js +75 -0
  30. package/dist/src/registry.js +8 -0
  31. package/dist/src/rule-metadata.d.ts +11 -0
  32. package/dist/src/rule-metadata.js +17 -0
  33. package/dist/src/rule-plan.d.ts +10 -11
  34. package/dist/src/rule-plan.js +117 -0
  35. package/dist/src/rules/array.d.ts +7 -6
  36. package/dist/src/rules/array.js +96 -0
  37. package/dist/src/rules/common.js +77 -0
  38. package/dist/src/rules/date.js +35 -0
  39. package/dist/src/rules/index.d.ts +2 -4
  40. package/dist/src/rules/index.js +8 -21
  41. package/dist/src/rules/locales.d.ts +5 -4
  42. package/dist/src/rules/locales.js +249 -0
  43. package/dist/src/rules/number.js +79 -0
  44. package/dist/src/rules/object.d.ts +1 -1
  45. package/dist/src/rules/object.js +49 -0
  46. package/dist/src/rules/string.d.ts +83 -80
  47. package/dist/src/rules/string.js +1998 -0
  48. package/dist/src/rules/typechecker.js +143 -0
  49. package/dist/src/seal/circular-analyzer.js +63 -0
  50. package/dist/src/seal/codegen-utils.js +18 -0
  51. package/dist/src/seal/deserialize-builder.d.ts +8 -4
  52. package/dist/src/seal/deserialize-builder.js +1546 -0
  53. package/dist/src/seal/expose-validator.d.ts +3 -2
  54. package/dist/src/seal/expose-validator.js +65 -0
  55. package/dist/src/seal/seal-state.d.ts +10 -0
  56. package/dist/src/seal/seal-state.js +18 -0
  57. package/dist/src/seal/seal.d.ts +22 -21
  58. package/dist/src/seal/seal.js +431 -0
  59. package/dist/src/seal/serialize-builder.d.ts +3 -2
  60. package/dist/src/seal/serialize-builder.js +374 -0
  61. package/dist/src/seal/validate-meta.d.ts +13 -0
  62. package/dist/src/seal/validate-meta.js +61 -0
  63. package/dist/src/symbols.d.ts +1 -1
  64. package/dist/src/symbols.js +13 -2
  65. package/dist/src/transformers/collection.transformer.js +25 -0
  66. package/dist/src/transformers/date.transformer.js +18 -0
  67. package/dist/src/transformers/index.js +6 -2
  68. package/dist/src/transformers/luxon.transformer.d.ts +4 -2
  69. package/dist/src/transformers/luxon.transformer.js +34 -0
  70. package/dist/src/transformers/moment.transformer.d.ts +4 -2
  71. package/dist/src/transformers/moment.transformer.js +32 -0
  72. package/dist/src/transformers/number.transformer.js +8 -0
  73. package/dist/src/transformers/string.transformer.js +12 -0
  74. package/dist/src/types.d.ts +27 -25
  75. package/dist/src/types.js +1 -0
  76. package/dist/src/utils.d.ts +2 -2
  77. package/dist/src/utils.js +10 -0
  78. package/package.json +80 -68
  79. package/dist/index-03cysbck.js +0 -3
  80. package/dist/index-dcbd798a.js +0 -3
  81. package/dist/index-jp2yjd6g.js +0 -3
  82. package/dist/index-mw7met6r.js +0 -3
  83. package/dist/index-xdn55cz3.js +0 -1
  84. package/dist/src/functions/_run-sealed.d.ts +0 -7
  85. package/dist/src/functions/index.d.ts +0 -3
  86. package/dist/src/seal/index.d.ts +0 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,263 @@
1
+ # @zipbul/baker
2
+
3
+ ## 3.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ea40a75: Docs/metadata accuracy: every README example now includes the required `@Recipe` decorator
8
+ (without it `seal()` does not register the class and `deserialize` throws), drop the
9
+ unnecessary `() => Set as any` / `Map as any`, state the Bun-only requirement (Bun ≥ 1.3.13),
10
+ and replace unsubstantiated speed multipliers with honest qualitative claims. Refresh the
11
+ package description and remove the now-redundant MIGRATION-3.0.md (its content lives in the
12
+ CHANGELOG 3.0.0 entry).
13
+
14
+ ## 3.0.0
15
+
16
+ ### Major Changes
17
+
18
+ - 421fd54: 3.0 — error system redesign and API hardening (breaking).
19
+
20
+ **Error channel.** A single `BakerError` class is now thrown for every developer/config/schema
21
+ misuse (it carries `cause`). The validation-result types are renamed for clarity:
22
+
23
+ - `SealError` → `BakerError` (the thrown class)
24
+ - the field-error interface `BakerError` → `BakerIssue`
25
+ - `BakerErrors` → `BakerIssueSet`
26
+ - `isBakerError` → `isBakerIssueSet`
27
+
28
+ The split is now explicit: **throw `BakerError`** for misuse discoverable without input;
29
+ **return `BakerIssueSet`** for external-input validation failures from `deserialize`/`validate`.
30
+
31
+ **API hardening.** `validate(Class, input)` is DTO-only (the ad-hoc `validate(value, ...rules)`
32
+ mode was removed — call a rule directly instead). `configure()` rejects unknown keys and
33
+ post-`seal()` calls, and seal-time options can no longer be passed per-call.
34
+
35
+ ### Minor Changes
36
+
37
+ - 421fd54: Add the `isHttpToken` rule — validates the RFC 9110 §5.6.2 HTTP `token` production
38
+ (`1*tchar`), used for HTTP method names and header field-names. Usable as a predicate
39
+ (`isHttpToken(value)`) or as `@Field(isHttpToken)`, and exported from `@zipbul/baker/rules`.
40
+
41
+ ### DX reform — breaking changes
42
+
43
+ - **Auto-seal removed.** Call `seal()` once at app startup, after every DTO module is loaded. Without it, the first `deserialize` / `serialize` / `validate` call throws `BakerError`.
44
+ - Migration: import `seal` and call `seal()` once before any deserialize/serialize/validate call. For tests, call `seal()` after each `unseal()` / `configure(...)` reconfiguration.
45
+ - **Per-call options are validated.** Only `groups` is a valid per-call option. Passing any other key (`stopAtFirstError`, `autoConvert`, `allowClassDefaults`, `forbidUnknown`, `debug`, …) throws `BakerError`. Move those keys into `configure({...})` before `seal()`.
46
+ - **`@Field` argument validation.** Passing a non-rule value (e.g. `@Field(isNumber)` instead of `@Field(isNumber())`) now throws `BakerError` immediately with the four valid forms listed in the message.
47
+ - **Map non-string keys.** Serializing a `Map<K, V>` whose key is not a `string` throws `BakerError` — previously the key was silently coerced via `[object Object]` and collided.
48
+
49
+ ### API additions
50
+
51
+ - `seal(...classes?)` — explicit AOT seal trigger.
52
+ - `deserializeSync<T>` / `deserializeAsync<T>` / `serializeSync<T>` / `serializeAsync<T>` / `validateSync` / `validateAsync` — strict variants. `*Sync` throws `BakerError` when the DTO is async on the relevant direction; `*Async` always returns `Promise`.
53
+
54
+ ### Defect fixes
55
+
56
+ - **F-1** `circular-analyzer.walk()` now walks `meta.type.collectionValue` — Set/Map nested DTO cycles are caught at seal time, no more `stack overflow` at runtime.
57
+ - **F-2** Discriminator / Set·Map / inheritance invariants now run before codegen via the new `validate-meta` pass — invalid metadata throws `BakerError` with a precise message instead of producing invalid generated JS.
58
+ - **F-3** Discriminator default branch now reports `context: { received, validSubTypes: [...] }` so callers can show the user the allowed values.
59
+ - **F-4** Per-call options other than `groups` are rejected with `BakerError` instead of being silently dropped.
60
+ - **F-8** FR passport regex now anchors both ends (`/^[A-Z0-9]{9}$/i`).
61
+ - **F-9** `MAGNET_URI_RE` is anchored on the trailing end.
62
+ - **N-3** Circular-detection `WeakSet` is now allocated per call via `Symbol.for('baker:circular-seen')` threaded through `_opts` — concurrent async calls no longer false-circular on shared input objects.
63
+ - **N-4** `extractCode` checks `Object.hasOwn(input, key)` before reading — prototype-chain values no longer leak into DTO results.
64
+ - **N-6** `mergeInheritance` validation dedup now compares by `ruleName`, so a child redeclaring the same rule (e.g. `minLength(5)`) replaces the parent's rule instead of producing duplicate errors.
65
+
66
+ ### Dead code
67
+
68
+ - `src/functions/_run-sealed.ts` removed. The corresponding internal-only tests in `test/e2e/change-coverage.test.ts` were dropped — their coverage is now provided by public-API tests.
69
+
70
+ ## 2.2.0
71
+
72
+ ### Minor Changes
73
+
74
+ - 78d701a: feat: validate-only executor with inline nested code generation
75
+
76
+ - Add `_validate` sealed executor — validates input without Object.create or property assignment
77
+ - validate() now uses dedicated `_validate` executor instead of routing through `_deserialize`
78
+ - Inline nested DTO validation: nested DTO fields are expanded directly into the parent function body, eliminating per-item function call overhead
79
+ - Recursive inline for all nesting patterns: nested objects, arrays of nested, discriminator, collections (Set/Map), transforms, groups
80
+ - Only circular references fall back to function call (physically impossible to inline)
81
+ - 14 refs-based validators converted to inline emit (isISBN, isISIN, isIBAN, isFQDN, etc.)
82
+ - Type gate dead code removal: 11 redundant checks eliminated in gated paths
83
+ - Rule Plan IR `stripSelfComparison` for AST-level optimization inside type gates
84
+ - Shared codegen utilities extracted to `codegen-utils.ts`
85
+ - GEN constants centralized in serialize-builder to prevent typo-related bugs
86
+ - `makeRule`/`makePlannedRule` factory functions for cleaner rule creation
87
+ - Sync/async contract enforcement for declared-sync rules
88
+
89
+ Performance:
90
+
91
+ - validate() nested 3-level: 8.79ns (typebox: 11.56ns) — 1.3x faster than typebox
92
+ - validate() array 1000 items: 2.35µs (typebox: 2.37µs) — equivalent to typebox
93
+ - validate() vs deserialize(): 2-5x faster across all benchmarks
94
+ - Zero memory leaks verified under 10M sustained operations
95
+ - 26.6M ops/sec throughput (validate valid, flat DTO)
96
+
97
+ ## 2.1.0
98
+
99
+ ### Minor Changes
100
+
101
+ - 5696199: feat: Transformer interface, built-in transformers, isULID, isCUID2
102
+
103
+ ### Breaking Changes
104
+
105
+ - `FieldOptions.transform` now accepts `Transformer | Transformer[]` instead of function
106
+ - `FieldTransformParams`, `FieldTransformFn` types removed — use `Transformer` interface
107
+ - `transformDirection` option removed — use passthrough in the unused direction method
108
+ - Serialize direction applies transforms in reverse order (codec stack)
109
+
110
+ ### New Features
111
+
112
+ - `Transformer` interface with separate `deserialize`/`serialize` methods
113
+ - `transform` option accepts arrays — serialize applies in reverse order
114
+ - `type` + `transform` combination support in serialize (nested serialize → transform)
115
+ - 9 built-in core transformers: trim, toLowerCase, toUpperCase, round, unixSeconds, unixMillis, isoString, csv, json
116
+ - 2 optional peer transformers: luxon, moment (async factory, `await import()`)
117
+ - `isULID()` validator
118
+ - `isCUID2()` validator
119
+ - `@zipbul/baker/transformers` subpath export
120
+
121
+ ### Improvements
122
+
123
+ - `when` callback typed as `(obj: Record<string, any>)` instead of `any`
124
+ - Sourcemap removed from build output
125
+ - README rewritten with GEO optimization (FAQ, benchmarks, comparison tables)
126
+ - package.json description and keywords optimized
127
+
128
+ ## 2.0.0
129
+
130
+ ### Major Changes
131
+
132
+ - 5d01955: feat!: v2 API overhaul — isBakerError, validate, performance optimization
133
+
134
+ ### Breaking Changes
135
+
136
+ - `deserialize()` no longer throws on validation failure — returns `T | BakerErrors | Promise<T | BakerErrors>`
137
+ - `serialize()` returns directly for sync DTOs — `Record<string, unknown> | Promise<Record<string, unknown>>`
138
+ - `BakerValidationError` class removed — use `isBakerError()` type guard
139
+ - `toJsonSchema()` removed
140
+ - `@Schema` decorator and `schema` field option removed
141
+ - `JsonSchemaOverride`, `ToJsonSchemaOptions` types removed
142
+ - `BAKER_ERROR` symbol no longer exported (internal only)
143
+ - `README.ko.md` removed
144
+
145
+ ### New Features
146
+
147
+ - `validate(Class, input, options?)` — DTO-level validation without instantiation
148
+ - `validate(input, ...rules)` — ad-hoc single value validation
149
+ - `isBakerError()` — type guard for narrowing validation results
150
+ - Sync DTOs return directly (no Promise wrapper) across all APIs
151
+ - Memory leak detection CI step
152
+
153
+ ### Performance
154
+
155
+ - Valid path: 188ns → 38ns (5x improvement)
156
+ - Invalid path: 6.08µs → 76ns (80x improvement)
157
+
158
+ ### Bug Fixes
159
+
160
+ - WeakSet circular detection false positive on same-object reuse
161
+ - serialize-builder async discriminator array syntax error
162
+ - 13 rules missing constraints in metadata
163
+
164
+ ## 1.1.0
165
+
166
+ ### Minor Changes
167
+
168
+ - b27cdf6: ## New Features
169
+
170
+ - **Sync API optimization** — `deserialize()` and `serialize()` are no longer `async function`. Sync DTOs (no async transforms/rules) skip `Promise` allocation via `Promise.resolve()`. Async DTOs use the executor's native `Promise`. Return type remains `Promise<T>` for backward compatibility.
171
+
172
+ - **Map/Set auto-conversion** — New `type: () => Map` and `type: () => Set` support in `@Field()`:
173
+
174
+ - `Set<T>`: JSON array ↔ `Set`, with optional `setValue: () => DtoClass` for nested DTOs
175
+ - `Map<string, T>`: JSON object ↔ `Map`, with optional `mapValue: () => DtoClass` for nested DTOs
176
+ - JSON Schema: Set → `{ type: 'array', uniqueItems: true }`, Map → `{ type: 'object', additionalProperties }`
177
+
178
+ - **Per-field error messages** — `message` and `context` options on `@Field()` apply to all rules on the field. Supports static strings, dynamic functions with `{ property, value, constraints }`, and arbitrary context values including falsy ones (`0`, `false`, `''`).
179
+
180
+ ## Chores
181
+
182
+ - Translate all Korean comments and documentation to English (82 files)
183
+ - Delete REVIEW.md (all 42 items completed)
184
+ - 1808 tests, 2639 assertions
185
+
186
+ ## 1.0.0
187
+
188
+ ### Major Changes
189
+
190
+ - b7ea675: ## Breaking Changes
191
+
192
+ - **`@Field()` unified decorator** — Replaces 30+ individual decorators with a single `@Field()` that accepts rules as arguments and options as an object.
193
+ - **Auto-seal** — `seal()` removed. DTOs auto-seal on first `deserialize()`/`serialize()` call.
194
+ - **`configure()` replaces `seal()` options** — `configure({ autoConvert, stopAtFirstError, forbidUnknown, ... })`.
195
+ - **`configure()` returns `{ warnings: string[] }`** instead of `void`.
196
+ - **`enableCircularCheck` removed** — Circular detection always runs automatically.
197
+ - **`stripUnknown` renamed to `forbidUnknown`** — `stripUnknown` kept as deprecated alias.
198
+
199
+ ## Bug Fixes
200
+
201
+ - C-1: Fix analyzeAsync discriminator visited Set sharing (infinite recursion risk)
202
+ - C-2: Fix Set/Map stopAtFirstError error path missing element index
203
+ - C-3: Fix discriminator JSON Schema $ref+properties sibling (allOf wrapper)
204
+ - C-5: Throw on isDivisibleBy(0)
205
+ - C-6: Fix isURL accepting ports 65536-99999
206
+ - C-7: Fix isNumber maxDecimalPlaces scientific notation bypass
207
+ - C-8: Implement serialize discriminator with instanceof dispatch
208
+ - C-9: Fix nullable $ref invalid JSON Schema (oneOf wrapper)
209
+ - C-11: Null guard for nested array serialize
210
+ - C-12: Throw on min(NaN)/max(Infinity)
211
+ - C-13~C-17, B-1~B-11: 11 additional safety guards and silent failure fixes
212
+
213
+ ## New Features
214
+
215
+ - Debug mode: `configure({ debug: true })`
216
+ - `onUnmappedRule` callback for `toJsonSchema()`
217
+ - `forbidUnknown` option (renamed from `stripUnknown`)
218
+
219
+ ## Refactoring
220
+
221
+ - Decompose buildRulesCode (250 lines → 5 functions) and Field() (125 lines → 4 helpers)
222
+ - Deduplicate Array/Set/Map each codegen, extract GEN constants, strategy pattern for nullable/optional
223
+ - 1730 tests, 2509 assertions, 99.94% Funcs / 99.83% Lines
224
+
225
+ ## 0.1.2
226
+
227
+ ### Patch Changes
228
+
229
+ - 76657db: fix: pin CI Bun version to 1.3.9 to avoid 1.3.10 bundler regression, optimize isIn/isNotIn with Set, improve npm packaging and test coverage
230
+
231
+ ## 0.1.1
232
+
233
+ ### Patch Changes
234
+
235
+ - 95ce993: Add coverage badge gist configuration
236
+
237
+ ## 0.1.0
238
+
239
+ ### Minor Changes
240
+
241
+ - 214f664: ### Breaking Changes
242
+
243
+ - Remove `src/aot/` module and `@zipbul/baker/aot` subpath export. The zipbul CLI now reads baker decorators directly via AST.
244
+ - `MessageArgs.constraints` type changed from `unknown[]` to `Record<string, unknown>`.
245
+ - Default behavior for fields without `@IsOptional`/`@IsNullable`: `undefined`/`null` input now emits `isDefined` error code instead of falling through to type gate errors (e.g., `isString`).
246
+
247
+ ### Features
248
+
249
+ - **`@Nested(fn, opts?)`** — Single-decorator shorthand for `@ValidateNested()` + `@Type(fn)` with discriminator support.
250
+ - **`@IsNullable()`** — Allow `null` (skip validation), reject `undefined`. Complements `@IsOptional()` for OAS 3.0 `nullable: true` semantics.
251
+ - **`@Schema(schema)`** — Attach JSON Schema Draft 2020-12 metadata at class or property level. Supports object and function forms.
252
+ - **`toJsonSchema(Class, opts?)`** — Generate JSON Schema Draft 2020-12 from DTO decorators. Supports `direction`, `groups`, circular references, discriminator `oneOf`, and `@Schema()` overrides.
253
+ - **`seal({ whitelist: true })`** — Reject undeclared fields with `whitelistViolation` error code.
254
+ - **`@Min(n, { exclusive: true })` / `@Max(n, { exclusive: true })`** — Exclusive minimum/maximum support.
255
+ - **`enableImplicitConversion`** — Automatic type conversion (string/number/boolean/date) based on `requiresType` and `@Type()` hints.
256
+ - **`EmittableRule.constraints`** — All built-in rules now expose their parameters via `constraints` for JSON Schema mapping and `message` callback access.
257
+ - **`requiresType` expansion** — Added `'boolean'` and `'date'` variants. Fixed silent rule loss for non-string/non-number `requiresType` values.
258
+
259
+ ### Internal
260
+
261
+ - Upgrade `@zipbul/result` from `^0.0.3` to `^0.1.4` and adopt `Result<T, E>` / `ResultAsync<T, E>` type aliases.
262
+ - Fix seal placeholder to throw `SealError` instead of bare `Error`.
263
+ - Remove dead branch in deserialize input type guard.
package/README.md CHANGED
@@ -6,28 +6,36 @@ The fastest decorator-based DTO validation library for TypeScript. Generates opt
6
6
  bun add @zipbul/baker
7
7
  ```
8
8
 
9
- Zero `reflect-metadata`. Sealed codegen. 1,975 tests. 99%+ line coverage.
9
+ Zero `reflect-metadata`. Sealed codegen. 99%+ line coverage.
10
+
11
+ > **Requires Bun ≥ 1.3.13.** baker relies on TC39 decorator metadata (`Symbol.metadata`), which Node does not populate — it is Bun-only.
10
12
 
11
13
  ## Quick Start
12
14
 
13
15
  ```typescript
14
- import { deserialize, isBakerError, Field } from '@zipbul/baker';
16
+ import { deserialize, isBakerIssueSet, Field, Recipe, seal } from '@zipbul/baker';
15
17
  import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
16
18
 
19
+ @Recipe
17
20
  class UserDto {
18
21
  @Field(isString, minLength(2)) name!: string;
19
22
  @Field(isNumber(), min(0)) age!: number;
20
23
  @Field(isString, isEmail()) email!: string;
21
24
  }
22
25
 
26
+ // Call once at app startup, after all DTOs are loaded.
27
+ seal();
28
+
23
29
  const result = await deserialize(UserDto, {
24
- name: 'Alice', age: 30, email: 'alice@test.com',
30
+ name: 'Alice',
31
+ age: 30,
32
+ email: 'alice@test.com',
25
33
  });
26
34
 
27
- if (isBakerError(result)) {
35
+ if (isBakerIssueSet(result)) {
28
36
  console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
29
37
  } else {
30
- console.log(result.name); // 'Alice' — typed as UserDto
38
+ console.log(result.name); // 'Alice' — typed as UserDto
31
39
  }
32
40
  ```
33
41
 
@@ -35,14 +43,14 @@ if (isBakerError(result)) {
35
43
 
36
44
  Baker generates optimized JavaScript functions once on first seal, then executes them on every call.
37
45
 
38
- | Feature | baker | class-validator | Zod |
39
- |---|---|---|---|
40
- | Valid path (5 fields) | **fast sealed path** | slower | slower |
41
- | Invalid path (5 fields) | **fast sealed path** | slower | slower |
42
- | Approach | AOT code generation | Runtime interpretation | Schema method chain |
43
- | Decorators | `@Field` (unified) | 30+ individual | N/A |
44
- | `reflect-metadata` | Not needed | Required | N/A |
45
- | Sync DTO return | Direct value | Promise | Direct value |
46
+ | Feature | baker | class-validator | Zod |
47
+ | ----------------------- | -------------------- | ---------------------- | ------------------- |
48
+ | Valid path (5 fields) | **fast sealed path** | slower | slower |
49
+ | Invalid path (5 fields) | **fast sealed path** | slower | slower |
50
+ | Approach | AOT code generation | Runtime interpretation | Schema method chain |
51
+ | Decorators | `@Field` (unified) | 30+ individual | N/A |
52
+ | `reflect-metadata` | Not needed | Required | N/A |
53
+ | Sync DTO return | Direct value | Promise | Direct value |
46
54
 
47
55
  ## Performance
48
56
 
@@ -52,43 +60,67 @@ See [`bench/`](./bench) for the current benchmark suite and exact scenarios.
52
60
 
53
61
  ## API
54
62
 
63
+ ### `seal(...classes?)`
64
+
65
+ **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.
66
+
67
+ `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.
68
+
55
69
  ### `deserialize<T>(Class, input, options?)`
56
70
 
57
- Returns `T | BakerErrors` for sync DTOs, `Promise<T | BakerErrors>` for async DTOs. Never throws on validation failure.
71
+ Returns `T | BakerIssueSet` for sync DTOs, `Promise<T | BakerIssueSet>` for async DTOs. Never throws on validation failure.
72
+
73
+ 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).
74
+
75
+ ### `deserializeSync<T>` / `deserializeAsync<T>`
76
+
77
+ 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`).
58
78
 
59
79
  ### `serialize<T>(instance, options?)`
60
80
 
61
- Returns `Record<string, unknown>` for sync DTOs, `Promise<Record<string, unknown>>` for async DTOs. No validation.
81
+ 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.
82
+
83
+ ### `serializeSync<T>` / `serializeAsync<T>`
84
+
85
+ Strict variants. `serializeSync` throws `BakerError` if the DTO is async on the serialize side.
86
+
87
+ ### `validate(Class, input, options?)`
62
88
 
63
- ### `validate(Class, input, options?)` / `validate(input, ...rules)`
89
+ 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)`).
64
90
 
65
- DTO-level or ad-hoc single-value validation. Returns `true | BakerErrors` for sync paths, `Promise<true | BakerErrors>` for async paths.
91
+ ### `validateSync` / `validateAsync`
66
92
 
67
- ### `isBakerError(value)`
93
+ Strict variants. `validateSync` throws `BakerError` if the DTO is async; `validateAsync` always returns `Promise`.
68
94
 
69
- Type guard. Narrows result to `BakerErrors` containing `{ path, code, message?, context? }[]`.
95
+ ### `isBakerIssueSet(value)`
96
+
97
+ Type guard. Narrows result to `BakerIssueSet` containing `{ path, code, message?, context? }[]`.
70
98
 
71
99
  ### `configure(config)`
72
100
 
73
- Global configuration. Call before first deserialize/serialize/validate. Calling it after auto-seal throws `SealError`.
101
+ Global configuration. Must be called **before** `seal()`. After seal, `configure(...)` throws `BakerError`; reconfiguring requires `unseal()` (test-only helper) + `configure(...)` + `seal()` again.
74
102
 
75
103
  ```typescript
76
104
  configure({
77
- autoConvert: true, // coerce "123" → 123
105
+ autoConvert: true, // coerce "123" → 123
78
106
  allowClassDefaults: true, // use class field initializers for missing keys
79
- stopAtFirstError: true, // return on first validation failure
80
- forbidUnknown: true, // reject undeclared fields
107
+ stopAtFirstError: true, // return on first validation failure
108
+ forbidUnknown: true, // reject undeclared fields
81
109
  });
82
110
  ```
83
111
 
84
- ### `createRule(name, validate)`
112
+ ### `createRule(name, validate)` / `createRule(options)`
85
113
 
86
- Custom validation rule.
114
+ Custom validation rule. Two forms — a `(name, validate)` shorthand or an options object:
115
+
116
+ ```typescript
117
+ const koreanPhone = createRule('koreanPhone', v => /^01[016789]/.test(v as string));
118
+ ```
87
119
 
88
120
  ```typescript
89
121
  const isEven = createRule({
90
122
  name: 'isEven',
91
- validate: (v) => typeof v === 'number' && v % 2 === 0,
123
+ validate: v => typeof v === 'number' && v % 2 === 0,
92
124
  requiresType: 'number',
93
125
  });
94
126
  ```
@@ -97,31 +129,37 @@ const isEven = createRule({
97
129
 
98
130
  One decorator for everything — replaces 30+ individual decorators from class-validator.
99
131
 
132
+ 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.
133
+
100
134
  ```typescript
101
135
  @Field(...rules)
102
136
  @Field(...rules, options)
103
137
  @Field(options)
138
+ @Field() // marker-only (no rules)
104
139
  ```
105
140
 
141
+ 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.
142
+
106
143
  ### Options
107
144
 
108
- | Option | Type | Description |
109
- |---|---|---|
110
- | `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
111
- | `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
112
- | `optional` | `boolean` | Allow undefined |
113
- | `nullable` | `boolean` | Allow null |
114
- | `name` | `string` | Bidirectional key mapping |
115
- | `deserializeName` | `string` | Input key mapping |
116
- | `serializeName` | `string` | Output key mapping |
117
- | `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
118
- | `groups` | `string[]` | Conditional visibility |
119
- | `when` | `(obj) => boolean` | Conditional validation |
120
- | `transform` | `Transformer \| Transformer[]` | Value transformer |
121
- | `message` | `string \| (args) => string` | Error message override |
122
- | `context` | `unknown` | Error context |
123
- | `mapValue` | `() => Dto` | Map value DTO |
124
- | `setValue` | `() => Dto` | Set element DTO |
145
+ | Option | Type | Description |
146
+ | ----------------- | ------------------------------------------------- | ------------------------------ |
147
+ | `type` | `() => Dto \| [Dto]` | Nested DTO. `[Dto]` for arrays |
148
+ | `discriminator` | `{ property, subTypes }` | Polymorphic dispatch |
149
+ | `keepDiscriminatorProperty` | `boolean` | Keep the discriminator key in the result |
150
+ | `optional` | `boolean` | Allow undefined |
151
+ | `nullable` | `boolean` | Allow null |
152
+ | `name` | `string` | Bidirectional key mapping |
153
+ | `deserializeName` | `string` | Input key mapping |
154
+ | `serializeName` | `string` | Output key mapping |
155
+ | `exclude` | `boolean \| 'deserializeOnly' \| 'serializeOnly'` | Field exclusion |
156
+ | `groups` | `string[]` | Conditional visibility |
157
+ | `when` | `(obj) => boolean` | Conditional validation |
158
+ | `transform` | `Transformer \| Transformer[]` | Value transformer |
159
+ | `message` | `string \| (args) => string` | Error message override |
160
+ | `context` | `unknown` | Error context |
161
+ | `mapValue` | `() => Dto` | Map value DTO |
162
+ | `setValue` | `() => Dto` | Set element DTO |
125
163
 
126
164
  ## Transformers
127
165
 
@@ -131,8 +169,8 @@ Bidirectional value transformers with separate `deserialize` and `serialize` met
131
169
  import type { Transformer } from '@zipbul/baker';
132
170
 
133
171
  const centsTransformer: Transformer = {
134
- deserialize: ({ value }) => typeof value === 'number' ? value * 100 : value,
135
- serialize: ({ value }) => typeof value === 'number' ? value / 100 : value,
172
+ deserialize: ({ value }) => (typeof value === 'number' ? value * 100 : value),
173
+ serialize: ({ value }) => (typeof value === 'number' ? value / 100 : value),
136
174
  };
137
175
  ```
138
176
 
@@ -140,27 +178,34 @@ const centsTransformer: Transformer = {
140
178
 
141
179
  ```typescript
142
180
  import {
143
- trimTransformer, toLowerCaseTransformer, toUpperCaseTransformer,
144
- roundTransformer, unixSecondsTransformer, unixMillisTransformer,
145
- isoStringTransformer, csvTransformer, jsonTransformer,
181
+ trimTransformer,
182
+ toLowerCaseTransformer,
183
+ toUpperCaseTransformer,
184
+ roundTransformer,
185
+ unixSecondsTransformer,
186
+ unixMillisTransformer,
187
+ isoStringTransformer,
188
+ csvTransformer,
189
+ jsonTransformer,
146
190
  } from '@zipbul/baker/transformers';
147
191
  ```
148
192
 
149
- | Transformer | deserialize | serialize |
150
- |---|---|---|
151
- | `trimTransformer` | trim string | trim string |
152
- | `toLowerCaseTransformer` | lowercase | lowercase |
153
- | `toUpperCaseTransformer` | uppercase | uppercase |
154
- | `roundTransformer(n?)` | round to n decimals | round to n decimals |
155
- | `unixSecondsTransformer` | unix seconds &rarr; Date | Date &rarr; unix seconds |
156
- | `unixMillisTransformer` | unix ms &rarr; Date | Date &rarr; unix ms |
157
- | `isoStringTransformer` | ISO string &rarr; Date | Date &rarr; ISO string |
158
- | `csvTransformer(sep?)` | `"a,b"` &rarr; `["a","b"]` | `["a","b"]` &rarr; `"a,b"` |
159
- | `jsonTransformer` | JSON string &rarr; object | object &rarr; JSON string |
193
+ | Transformer | deserialize | serialize |
194
+ | ------------------------ | -------------------------- | -------------------------- |
195
+ | `trimTransformer` | trim string | trim string |
196
+ | `toLowerCaseTransformer` | lowercase | lowercase |
197
+ | `toUpperCaseTransformer` | uppercase | uppercase |
198
+ | `roundTransformer(n?)` | round to n decimals | round to n decimals |
199
+ | `unixSecondsTransformer` | unix seconds &rarr; Date | Date &rarr; unix seconds |
200
+ | `unixMillisTransformer` | unix ms &rarr; Date | Date &rarr; unix ms |
201
+ | `isoStringTransformer` | ISO string &rarr; Date | Date &rarr; ISO string |
202
+ | `csvTransformer(sep?)` | `"a,b"` &rarr; `["a","b"]` | `["a","b"]` &rarr; `"a,b"` |
203
+ | `jsonTransformer` | JSON string &rarr; object | object &rarr; JSON string |
160
204
 
161
205
  ### Transform Array Order
162
206
 
163
207
  Multiple transformers apply as a codec stack:
208
+
164
209
  - **Deserialize**: left to right — `[A, B, C]` applies A, then B, then C
165
210
  - **Serialize**: right to left — `[A, B, C]` applies C, then B, then A
166
211
 
@@ -178,6 +223,7 @@ email!: string;
178
223
  import { luxonTransformer } from '@zipbul/baker/transformers';
179
224
  const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
180
225
 
226
+ @Recipe
181
227
  class EventDto {
182
228
  @Field({ transform: luxon }) startAt!: DateTime;
183
229
  }
@@ -189,9 +235,11 @@ import { momentTransformer } from '@zipbul/baker/transformers';
189
235
  const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
190
236
  ```
191
237
 
238
+ > **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).
239
+
192
240
  ## Rules
193
241
 
194
- 104 built-in validation rules.
242
+ 105 built-in validation rules.
195
243
 
196
244
  ### Type Checkers
197
245
 
@@ -209,7 +257,7 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
209
257
 
210
258
  ### Formats
211
259
 
212
- `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`
260
+ `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`, `isHttpToken`
213
261
 
214
262
  ### Arrays
215
263
 
@@ -230,10 +278,12 @@ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
230
278
  ## Nested DTOs
231
279
 
232
280
  ```typescript
281
+ @Recipe
233
282
  class AddressDto {
234
283
  @Field(isString) city!: string;
235
284
  }
236
285
 
286
+ @Recipe
237
287
  class UserDto {
238
288
  @Field({ type: () => AddressDto }) address!: AddressDto;
239
289
  @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
@@ -243,15 +293,19 @@ class UserDto {
243
293
  ## Collections
244
294
 
245
295
  ```typescript
296
+ @Recipe
246
297
  class UserDto {
247
- @Field({ type: () => Set as any, setValue: () => TagDto }) tags!: Set<TagDto>;
248
- @Field({ type: () => Map as any, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
298
+ @Field({ type: () => Set, setValue: () => TagDto }) tags!: Set<TagDto>;
299
+ @Field({ type: () => Map, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
249
300
  }
250
301
  ```
251
302
 
303
+ > 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.
304
+
252
305
  ## Discriminator
253
306
 
254
307
  ```typescript
308
+ @Recipe
255
309
  class PetOwner {
256
310
  @Field({
257
311
  type: () => CatDto,
@@ -262,17 +316,20 @@ class PetOwner {
262
316
  { value: DogDto, name: 'dog' },
263
317
  ],
264
318
  },
265
- }) pet!: CatDto | DogDto;
319
+ })
320
+ pet!: CatDto | DogDto;
266
321
  }
267
322
  ```
268
323
 
269
324
  ## Inheritance
270
325
 
271
326
  ```typescript
327
+ @Recipe
272
328
  class BaseDto {
273
329
  @Field(isString) id!: string;
274
330
  }
275
331
 
332
+ @Recipe
276
333
  class UserDto extends BaseDto {
277
334
  @Field(isString) name!: string;
278
335
  // inherits 'id' field with isString rule
@@ -283,11 +340,11 @@ class UserDto extends BaseDto {
283
340
 
284
341
  ### When should I use baker instead of class-validator?
285
342
 
286
- When performance matters. baker is 163x faster on valid input and 109x faster on invalid input, while providing the same decorator-based DX. baker also eliminates the `reflect-metadata` dependency.
343
+ 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/`](./bench) to measure the exact difference on your machine.
287
344
 
288
345
  ### How does baker compare to Zod?
289
346
 
290
- Zod uses schema method chains (`z.string().email()`), baker uses decorators (`@Field(isString, isEmail())`). baker is 16x faster on valid input because it generates optimized code at definition time instead of interpreting schemas at runtime. Choose Zod if you need schema-first design; choose baker if you need class-based DTOs with maximum performance.
347
+ 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.
291
348
 
292
349
  ### Does baker support async validation?
293
350
 
@@ -299,13 +356,19 @@ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()
299
356
 
300
357
  ### How does the AOT code generation work?
301
358
 
302
- On the first call to `deserialize`/`serialize`/`validate`, baker seals all registered DTOs: it analyzes field metadata, generates optimized JavaScript validation functions via `new Function()`, and caches them. Subsequent calls execute the pre-compiled functions directly.
359
+ 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.
303
360
 
304
361
  ## Exports
305
362
 
306
363
  ```typescript
307
- import { deserialize, validate, serialize, configure, createRule, Field, arrayOf, isBakerError, SealError } from '@zipbul/baker';
308
- import type { Transformer, TransformParams, BakerError, BakerErrors, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
364
+ import {
365
+ seal,
366
+ deserialize, deserializeSync, deserializeAsync,
367
+ validate, validateSync, validateAsync,
368
+ serialize, serializeSync, serializeAsync,
369
+ configure, createRule, Field, arrayOf, isBakerIssueSet, BakerError,
370
+ } from '@zipbul/baker';
371
+ import type { Transformer, TransformParams, BakerError, BakerIssueSet, FieldOptions, EmittableRule, RuntimeOptions } from '@zipbul/baker';
309
372
  import { isString, isEmail, isULID, isCUID2, ... } from '@zipbul/baker/rules';
310
373
  import { trimTransformer, jsonTransformer, ... } from '@zipbul/baker/transformers';
311
374
  ```