@zipbul/baker 1.1.0 → 2.1.0

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 (38) hide show
  1. package/README.md +189 -501
  2. package/dist/index-fnv35wrf.js +3 -0
  3. package/dist/index-k3d659ad.js +3 -0
  4. package/dist/index-s0n74vx1.js +3 -0
  5. package/dist/index-xdn55cz3.js +0 -3
  6. package/dist/index.d.ts +5 -6
  7. package/dist/index.js +212 -211
  8. package/dist/src/collect.d.ts +0 -2
  9. package/dist/src/configure.d.ts +0 -6
  10. package/dist/src/create-rule.d.ts +1 -1
  11. package/dist/src/decorators/field.d.ts +4 -21
  12. package/dist/src/decorators/index.d.ts +1 -1
  13. package/dist/src/decorators/index.js +1 -4
  14. package/dist/src/errors.d.ts +19 -8
  15. package/dist/src/functions/_run-sealed.d.ts +7 -0
  16. package/dist/src/functions/deserialize.d.ts +5 -4
  17. package/dist/src/functions/index.d.ts +1 -2
  18. package/dist/src/functions/serialize.d.ts +2 -2
  19. package/dist/src/functions/validate.d.ts +13 -0
  20. package/dist/src/rules/index.d.ts +2 -2
  21. package/dist/src/rules/index.js +8 -11
  22. package/dist/src/rules/string.d.ts +8 -48
  23. package/dist/src/symbols.d.ts +0 -2
  24. package/dist/src/symbols.js +1 -4
  25. package/dist/src/transformers/collection.transformer.d.ts +3 -0
  26. package/dist/src/transformers/date.transformer.d.ts +4 -0
  27. package/dist/src/transformers/index.d.ts +8 -0
  28. package/dist/src/transformers/index.js +2 -0
  29. package/dist/src/transformers/luxon.transformer.d.ts +6 -0
  30. package/dist/src/transformers/moment.transformer.d.ts +5 -0
  31. package/dist/src/transformers/number.transformer.d.ts +2 -0
  32. package/dist/src/transformers/string.transformer.d.ts +4 -0
  33. package/dist/src/types.d.ts +9 -63
  34. package/package.json +40 -6
  35. package/README.ko.md +0 -588
  36. package/dist/index-70ggmxsa.js +0 -6
  37. package/dist/index-gcptd79v.js +0 -6
  38. package/dist/src/functions/to-json-schema.d.ts +0 -20
package/README.md CHANGED
@@ -1,627 +1,315 @@
1
- <p align="center">
2
- <h1 align="center">@zipbul/baker</h1>
3
- <p align="center">
4
- <strong>Decorator-based validate + transform with inline code generation</strong>
5
- </p>
6
- <p align="center">
7
- Single <code>@Field()</code> decorator &middot; AOT-level performance &middot; zero reflect-metadata
8
- </p>
9
- <p align="center">
10
- <a href="https://github.com/zipbul/baker/actions"><img src="https://github.com/zipbul/baker/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
11
- <a href="https://www.npmjs.com/package/@zipbul/baker"><img src="https://img.shields.io/npm/v/@zipbul/baker.svg" alt="npm version"></a>
12
- <a href="https://www.npmjs.com/package/@zipbul/baker"><img src="https://img.shields.io/npm/dm/@zipbul/baker.svg" alt="npm downloads"></a>
13
- <a href="https://github.com/zipbul/baker/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@zipbul/baker.svg" alt="license"></a>
14
- </p>
15
- </p>
16
-
17
- <p align="center">
18
- <a href="./README.ko.md">한국어</a>
19
- </p>
20
-
21
- ---
1
+ # @zipbul/baker
22
2
 
23
- ## Why Baker?
24
-
25
- | | class-validator | Zod | TypeBox | **Baker** |
26
- |---|---|---|---|---|
27
- | Schema style | Decorators | Function chaining | JSON Schema builder | **Single `@Field()` decorator** |
28
- | Performance | Runtime interpreter | Runtime interpreter | JIT compile | **`new Function()` inline codegen** |
29
- | Transform built-in | Separate package | `.transform()` | N/A | **Unified** |
30
- | reflect-metadata | Required | N/A | N/A | **Not needed** |
31
- | class-validator migration | — | Full rewrite | Full rewrite | **Near drop-in** |
32
-
33
- Baker gives you a **single `@Field()` decorator** that combines validation, transformation, exposure control, and type hints. At first use, it auto-seals all DTOs by generating optimized functions via `new Function()` — delivering **AOT-equivalent performance without a compiler plugin**.
34
-
35
- ---
36
-
37
- ## Features
38
-
39
- - **Single decorator** — `@Field()` replaces 30+ individual decorators
40
- - **80+ built-in rules** — `isString`, `min()`, `isEmail()` and more, composed as arguments
41
- - **Inline code generation** — auto-seal compiles validators at first `deserialize()`/`serialize()` call
42
- - **Unified validate + transform** — `deserialize()` and `serialize()` in one call (sync DTOs skip async overhead)
43
- - **Zero reflect-metadata** — no `reflect-metadata` import needed
44
- - **Circular reference detection** — automatic static analysis at seal time
45
- - **Group-based validation** — apply different rules per request with `groups`
46
- - **Custom rules** — `createRule()` for user-defined validators with codegen support
47
- - **JSON Schema output** — `toJsonSchema()` generates JSON Schema Draft 2020-12 from your DTOs
48
- - **Polymorphic discriminator** — `@Field({ discriminator })` for union types
49
- - **Whitelist mode** — reject undeclared fields with `configure({ forbidUnknown: true })`
50
- - **Class inheritance** — child DTOs inherit parent `@Field()` decorators automatically
51
- - **Async transforms** — transform functions can be async
52
- - **Map/Set support** — auto-convert between `Map`/`Set` and JSON-compatible types
53
- - **Per-field error messages** — `message` and `context` options on `@Field()` for custom errors
54
-
55
- ---
56
-
57
- ## Installation
3
+ The fastest decorator-based DTO validation library for TypeScript. Generates optimized validation code at class definition time (AOT), delivering **42ns per validation** — up to 163x faster than class-validator, 16x faster than Zod.
58
4
 
59
5
  ```bash
60
6
  bun add @zipbul/baker
61
7
  ```
62
8
 
63
- > **Requirements:** Bun >= 1.0, `experimentalDecorators: true` in tsconfig.json
9
+ Zero `reflect-metadata`. Zero runtime overhead. 1,890 tests. 99%+ line coverage.
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { deserialize, isBakerError, Field } from '@zipbul/baker';
15
+ import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
16
+
17
+ class UserDto {
18
+ @Field(isString, minLength(2)) name!: string;
19
+ @Field(isNumber(), min(0)) age!: number;
20
+ @Field(isString, isEmail()) email!: string;
21
+ }
22
+
23
+ const result = await deserialize(UserDto, {
24
+ name: 'Alice', age: 30, email: 'alice@test.com',
25
+ });
64
26
 
65
- ```jsonc
66
- // tsconfig.json
67
- {
68
- "compilerOptions": {
69
- "experimentalDecorators": true
70
- }
27
+ if (isBakerError(result)) {
28
+ console.log(result.errors); // [{ path: 'email', code: 'isEmail' }]
29
+ } else {
30
+ console.log(result.name); // 'Alice' — typed as UserDto
71
31
  }
72
32
  ```
73
33
 
74
- ---
34
+ ## Why Baker?
75
35
 
76
- ## Quick Start
36
+ Baker generates optimized JavaScript validation functions **once** at class definition time, then executes them on every call — no interpretation, no schema traversal, no runtime compilation cost after the first seal.
77
37
 
78
- ### 1. Define a DTO
38
+ | Feature | baker | class-validator | Zod |
39
+ |---|---|---|---|
40
+ | Valid path (5 fields) | **42ns** | 6,852ns | 675ns |
41
+ | Invalid path (5 fields) | **93ns** | 10,109ns | 7,948ns |
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 |
79
46
 
80
- ```typescript
81
- import { Field } from '@zipbul/baker';
82
- import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
47
+ ## Performance
83
48
 
84
- class CreateUserDto {
85
- @Field(isString)
86
- name!: string;
49
+ Benchmarked against 6 libraries on a simple 5-field DTO (valid + invalid input):
87
50
 
88
- @Field(isInt, min(0), max(120))
89
- age!: number;
51
+ | Library | Valid | Invalid | vs baker (valid) | vs baker (invalid) |
52
+ |---|---|---|---|---|
53
+ | **baker** | **42ns** | **93ns** | — | — |
54
+ | TypeBox | 123ns | 112ns | 2.9x slower | 1.2x slower |
55
+ | AJV | 142ns | 201ns | 3.4x slower | 2.2x slower |
56
+ | ArkType | 145ns | 8,591ns | 3.4x slower | 92x slower |
57
+ | Valibot | 281ns | 1,070ns | 6.7x slower | 12x slower |
58
+ | Zod | 675ns | 7,948ns | 16x slower | 85x slower |
59
+ | class-validator | 6,852ns | 10,109ns | 163x slower | 109x slower |
90
60
 
91
- @Field(isEmail())
92
- email!: string;
93
- }
94
- ```
61
+ ## API
95
62
 
96
- ### 2. Deserialize (auto-seals on first call)
63
+ ### `deserialize<T>(Class, input, options?)`
97
64
 
98
- ```typescript
99
- import { deserialize, BakerValidationError } from '@zipbul/baker';
100
-
101
- try {
102
- const user = await deserialize(CreateUserDto, requestBody);
103
- // user is a validated CreateUserDto instance
104
- } catch (e) {
105
- if (e instanceof BakerValidationError) {
106
- console.log(e.errors); // BakerError[]
107
- }
108
- }
109
- ```
65
+ Returns `T | BakerErrors | Promise<T | BakerErrors>`. Sync DTOs return directly — no Promise wrapping. Never throws on validation failure.
110
66
 
111
- ### 3. Serialize
67
+ ### `serialize<T>(instance, options?)`
112
68
 
113
- ```typescript
114
- import { serialize } from '@zipbul/baker';
69
+ Returns `Record<string, unknown> | Promise<Record<string, unknown>>`. No validation. Sync DTOs return directly.
115
70
 
116
- const plain = await serialize(userInstance);
117
- // plain: Record<string, unknown>
118
- ```
71
+ ### `validate(Class, input, options?)` / `validate(input, ...rules)`
119
72
 
120
- > No `seal()` call needed — baker auto-seals all registered DTOs on the first `deserialize()` or `serialize()` call.
73
+ DTO-level or ad-hoc single-value validation. Returns `true | BakerErrors`.
121
74
 
122
- ---
75
+ ### `isBakerError(value)`
123
76
 
124
- ## The `@Field()` Decorator
77
+ Type guard. Narrows result to `BakerErrors` containing `{ path, code, message?, context? }[]`.
125
78
 
126
- `@Field()` is the single decorator that replaces all individual decorators. It accepts validation rules as positional arguments and an options object for advanced features.
79
+ ### `configure(config)`
127
80
 
128
- ### Signatures
81
+ Global configuration. Call before first deserialize/serialize/validate.
129
82
 
130
83
  ```typescript
131
- // Rules only
132
- @Field(isString, minLength(3), maxLength(100))
84
+ configure({
85
+ autoConvert: true, // coerce "123" → 123
86
+ allowClassDefaults: true, // use class field initializers for missing keys
87
+ stopAtFirstError: true, // return on first validation failure
88
+ forbidUnknown: true, // reject undeclared fields
89
+ });
90
+ ```
133
91
 
134
- // Options only
135
- @Field({ optional: true, nullable: true })
92
+ ### `createRule(name, validate)`
136
93
 
137
- // Rules + options
138
- @Field(isString, { name: 'user_name', groups: ['create'] })
94
+ Custom validation rule with optional AOT `emit()` for maximum performance.
139
95
 
140
- // No rules (plain field)
141
- @Field()
142
- ```
96
+ ## @Field Decorator
143
97
 
144
- ### FieldOptions
98
+ One decorator for everything — replaces 30+ individual decorators from class-validator.
145
99
 
146
100
  ```typescript
147
- interface FieldOptions {
148
- type?: () => Constructor | [Constructor]; // Nested DTO type (thunk for circular refs)
149
- discriminator?: { // Polymorphic union
150
- property: string;
151
- subTypes: { value: Function; name: string }[];
152
- };
153
- keepDiscriminatorProperty?: boolean; // Keep discriminator key in output
154
- rules?: (EmittableRule | ArrayOfMarker)[]; // Validation rules (alternative to positional args)
155
- optional?: boolean; // Allow undefined
156
- nullable?: boolean; // Allow null
157
- name?: string; // JSON key mapping (bidirectional)
158
- deserializeName?: string; // Deserialize-only key mapping
159
- serializeName?: string; // Serialize-only key mapping
160
- exclude?: boolean | 'deserializeOnly' | 'serializeOnly';
161
- groups?: string[]; // Visibility + conditional validation
162
- when?: (obj: any) => boolean; // Conditional validation
163
- schema?: JsonSchemaOverride; // JSON Schema metadata
164
- transform?: (params: FieldTransformParams) => unknown;
165
- transformDirection?: 'deserializeOnly' | 'serializeOnly';
166
- message?: string | ((args: MessageArgs) => string); // Error message for all rules
167
- context?: unknown; // Error context for all rules
168
- mapValue?: () => Constructor; // Map value DTO type
169
- setValue?: () => Constructor; // Set element DTO type
170
- }
101
+ @Field(...rules)
102
+ @Field(...rules, options)
103
+ @Field(options)
171
104
  ```
172
105
 
173
- ### Per-field Error Messages
106
+ ### Options
174
107
 
175
- Use `message` and `context` to customize validation error output:
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 |
125
+
126
+ ## Transformers
127
+
128
+ Bidirectional value transformers with separate `deserialize` and `serialize` methods.
176
129
 
177
130
  ```typescript
178
- @Field(isString, minLength(3), { message: 'Name is invalid' })
179
- name!: string;
131
+ import type { Transformer } from '@zipbul/baker';
180
132
 
181
- @Field(isEmail(), {
182
- message: ({ property, value }) => `${property} got bad value: ${value}`,
183
- context: { severity: 'error' },
184
- })
185
- email!: string;
133
+ const centsTransformer: Transformer = {
134
+ deserialize: ({ value }) => typeof value === 'number' ? value * 100 : value,
135
+ serialize: ({ value }) => typeof value === 'number' ? value / 100 : value,
136
+ };
186
137
  ```
187
138
 
188
- The `message` and `context` are applied to all rules on the field. They appear in `BakerError.message` and `BakerError.context` on validation failure.
139
+ ### Built-in Transformers
189
140
 
190
- ### `arrayOf()` — Array Element Validation
141
+ ```typescript
142
+ import {
143
+ trimTransformer, toLowerCaseTransformer, toUpperCaseTransformer,
144
+ roundTransformer, unixSecondsTransformer, unixMillisTransformer,
145
+ isoStringTransformer, csvTransformer, jsonTransformer,
146
+ } from '@zipbul/baker/transformers';
147
+ ```
191
148
 
192
- `arrayOf()` applies rules to each element of an array. Import it from `@zipbul/baker/rules` or `@zipbul/baker`.
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 |
160
+
161
+ ### Transform Array Order
162
+
163
+ Multiple transformers apply as a codec stack:
164
+ - **Deserialize**: left to right — `[A, B, C]` applies A, then B, then C
165
+ - **Serialize**: right to left — `[A, B, C]` applies C, then B, then A
193
166
 
194
167
  ```typescript
195
- import { Field, arrayOf } from '@zipbul/baker';
196
- import { isString, minLength } from '@zipbul/baker/rules';
197
-
198
- class TagsDto {
199
- @Field(arrayOf(isString, minLength(1)))
200
- tags!: string[];
201
- }
168
+ @Field(isString, { transform: [trimTransformer, toLowerCaseTransformer] })
169
+ email!: string;
170
+ // deserialize " HELLO " → trim → toLowerCase → "hello"
171
+ // serialize "hello" → toLowerCase → trim → "hello"
202
172
  ```
203
173
 
204
- You can mix `arrayOf()` with top-level array rules:
174
+ ### Optional Peer Transformers
205
175
 
206
176
  ```typescript
207
- import { arrayMinSize, arrayMaxSize } from '@zipbul/baker/rules';
177
+ // bun add luxon
178
+ import { luxonTransformer } from '@zipbul/baker/transformers';
179
+ const luxon = await luxonTransformer({ zone: 'Asia/Seoul' });
208
180
 
209
- class ScoresDto {
210
- @Field(arrayMinSize(1), arrayMaxSize(10), arrayOf(isInt, min(0), max(100)))
211
- scores!: number[];
181
+ class EventDto {
182
+ @Field({ transform: luxon }) startAt!: DateTime;
212
183
  }
213
184
  ```
214
185
 
215
- ---
216
-
217
- ## Built-in Rules
186
+ ```typescript
187
+ // bun add moment
188
+ import { momentTransformer } from '@zipbul/baker/transformers';
189
+ const mt = await momentTransformer({ format: 'YYYY-MM-DD' });
190
+ ```
218
191
 
219
- All rules are imported from `@zipbul/baker/rules` and passed as arguments to `@Field()`.
192
+ ## Rules
220
193
 
221
- > **Constants vs factory functions:** Some rules are pre-built constants (used without `()`) while others are factory functions that accept parameters (used with `()`). The tables below mark constants with a dagger symbol.
194
+ 104 built-in validation rules.
222
195
 
223
196
  ### Type Checkers
224
197
 
225
- | Rule | Description |
226
- |---|---|
227
- | `isString` | `typeof === 'string'` |
228
- | `isNumber(opts?)` | `typeof === 'number'` with NaN/Infinity/maxDecimalPlaces checks |
229
- | `isInt` | Integer check |
230
- | `isBoolean` | `typeof === 'boolean'` |
231
- | `isDate` | `instanceof Date && !isNaN` |
232
- | `isEnum(enumObj)` | Enum value check |
233
- | `isArray` | `Array.isArray()` |
234
- | `isObject` | `typeof === 'object'`, excludes null/Array |
198
+ `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()` needed.
235
199
 
236
- > `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` are constants (no parentheses needed). `isNumber(opts?)` and `isEnum(enumObj)` are factory functions.
200
+ `isNumber(options?)`, `isEnum(entity)` factories, require `()`.
237
201
 
238
- ### Common
239
-
240
- | Rule | Description |
241
- |---|---|
242
- | `equals(val)` | Strict equality (`===`) |
243
- | `notEquals(val)` | Strict inequality (`!==`) |
244
- | `isEmpty` | `undefined`, `null`, or `''` |
245
- | `isNotEmpty` | Not `undefined`, `null`, or `''` |
246
- | `isIn(arr)` | Value is in the given array |
247
- | `isNotIn(arr)` | Value is not in the given array |
202
+ ### Numbers
248
203
 
249
- > `isEmpty` and `isNotEmpty` are constants. The rest are factory functions.
204
+ `min(n)`, `max(n)`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
250
205
 
251
- ### Number
206
+ ### Strings
252
207
 
253
- | Rule | Description |
254
- |---|---|
255
- | `min(n, opts?)` | `value >= n` (supports `{ exclusive: true }`) |
256
- | `max(n, opts?)` | `value <= n` (supports `{ exclusive: true }`) |
257
- | `isPositive` | `value > 0` |
258
- | `isNegative` | `value < 0` |
259
- | `isDivisibleBy(n)` | `value % n === 0` |
208
+ `minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
260
209
 
261
- > `isPositive` and `isNegative` are constants (no parentheses). `min()`, `max()`, and `isDivisibleBy()` are factory functions.
210
+ ### Formats
262
211
 
263
- ### String
212
+ `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber()`, `isStrongPassword()`, `isULID()`, `isCUID2()`
264
213
 
265
- All string rules require the value to be a `string` type.
214
+ ### Arrays
266
215
 
267
- | Rule | Kind | Description |
268
- |---|---|---|
269
- | `minLength(n)` | factory | Minimum length |
270
- | `maxLength(n)` | factory | Maximum length |
271
- | `length(min, max)` | factory | Length range |
272
- | `contains(seed)` | factory | Contains substring |
273
- | `notContains(seed)` | factory | Does not contain substring |
274
- | `matches(pattern, modifiers?)` | factory | Regex match |
275
- | `isLowercase` | constant | All lowercase |
276
- | `isUppercase` | constant | All uppercase |
277
- | `isAscii` | constant | ASCII only |
278
- | `isAlpha` | constant | Alphabetic only (en-US) |
279
- | `isAlphanumeric` | constant | Alphanumeric only (en-US) |
280
- | `isBooleanString` | constant | `'true'`, `'false'`, `'1'`, or `'0'` |
281
- | `isNumberString(opts?)` | factory | Numeric string |
282
- | `isDecimal(opts?)` | factory | Decimal string |
283
- | `isFullWidth` | constant | Full-width characters |
284
- | `isHalfWidth` | constant | Half-width characters |
285
- | `isVariableWidth` | constant | Mix of full-width and half-width |
286
- | `isMultibyte` | constant | Multibyte characters |
287
- | `isSurrogatePair` | constant | Surrogate pair characters |
288
- | `isHexadecimal` | constant | Hexadecimal string |
289
- | `isOctal` | constant | Octal string |
290
- | `isEmail(opts?)` | factory | Email format |
291
- | `isURL(opts?)` | factory | URL format (port range validated) |
292
- | `isUUID(version?)` | factory | UUID v1-v5 |
293
- | `isIP(version?)` | factory | IPv4 / IPv6 |
294
- | `isHexColor` | constant | Hex color (`#fff`, `#ffffff`) |
295
- | `isRgbColor(includePercent?)` | factory | RGB color string |
296
- | `isHSL` | constant | HSL color string |
297
- | `isMACAddress(opts?)` | factory | MAC address |
298
- | `isISBN(version?)` | factory | ISBN-10 / ISBN-13 |
299
- | `isISIN` | constant | ISIN (International Securities Identification Number) |
300
- | `isISO8601(opts?)` | factory | ISO 8601 date string |
301
- | `isISRC` | constant | ISRC (International Standard Recording Code) |
302
- | `isISSN(opts?)` | factory | ISSN (International Standard Serial Number) |
303
- | `isJWT` | constant | JSON Web Token |
304
- | `isLatLong(opts?)` | factory | Latitude/longitude string |
305
- | `isLocale` | constant | Locale string (e.g. `en_US`) |
306
- | `isDataURI` | constant | Data URI |
307
- | `isFQDN(opts?)` | factory | Fully qualified domain name |
308
- | `isPort` | constant | Port number string (0-65535) |
309
- | `isEAN` | constant | EAN (European Article Number) |
310
- | `isISO31661Alpha2` | constant | ISO 3166-1 alpha-2 country code |
311
- | `isISO31661Alpha3` | constant | ISO 3166-1 alpha-3 country code |
312
- | `isBIC` | constant | BIC (Bank Identification Code) / SWIFT code |
313
- | `isFirebasePushId` | constant | Firebase Push ID |
314
- | `isSemVer` | constant | Semantic version string |
315
- | `isMongoId` | constant | MongoDB ObjectId (24-char hex) |
316
- | `isJSON` | constant | Parseable JSON string |
317
- | `isBase32(opts?)` | factory | Base32 encoded |
318
- | `isBase58` | constant | Base58 encoded |
319
- | `isBase64(opts?)` | factory | Base64 encoded |
320
- | `isDateString(opts?)` | factory | Date string (configurable strict mode) |
321
- | `isMimeType` | constant | MIME type string |
322
- | `isCurrency(opts?)` | factory | Currency string |
323
- | `isMagnetURI` | constant | Magnet URI |
324
- | `isCreditCard` | constant | Credit card number (Luhn) |
325
- | `isIBAN(opts?)` | factory | IBAN |
326
- | `isByteLength(min, max?)` | factory | Byte length range |
327
- | `isHash(algorithm)` | factory | Hash string (md4, md5, sha1, sha256, sha384, sha512, etc.) |
328
- | `isRFC3339` | constant | RFC 3339 date-time string |
329
- | `isMilitaryTime` | constant | Military time (HH:MM) |
330
- | `isLatitude` | constant | Latitude string |
331
- | `isLongitude` | constant | Longitude string |
332
- | `isEthereumAddress` | constant | Ethereum address |
333
- | `isBtcAddress` | constant | Bitcoin address |
334
- | `isISO4217CurrencyCode` | constant | ISO 4217 currency code |
335
- | `isPhoneNumber` | constant | E.164 international phone number |
336
- | `isStrongPassword(opts?)` | factory | Strong password (configurable min length, uppercase, lowercase, numbers, symbols) |
337
- | `isTaxId(locale)` | factory | Tax ID for given locale |
338
-
339
- ### Array
340
-
341
- | Rule | Description |
342
- |---|---|
343
- | `arrayContains(values)` | Contains all given elements |
344
- | `arrayNotContains(values)` | Contains none of the given elements |
345
- | `arrayMinSize(n)` | Minimum array length |
346
- | `arrayMaxSize(n)` | Maximum array length |
347
- | `arrayUnique()` | No duplicates |
348
- | `arrayNotEmpty()` | Not empty |
216
+ `arrayMinSize(n)`, `arrayMaxSize(n)`, `arrayUnique()`, `arrayNotEmpty`, `arrayContains(values)`, `arrayOf(...rules)`
349
217
 
350
- ### Date
218
+ ### Common
351
219
 
352
- | Rule | Description |
353
- |---|---|
354
- | `minDate(date)` | Minimum date |
355
- | `maxDate(date)` | Maximum date |
220
+ `equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
356
221
 
357
- ### Object
222
+ ### Date
358
223
 
359
- | Rule | Description |
360
- |---|---|
361
- | `isNotEmptyObject(opts?)` | At least one key (supports `{ nullable: true }` to ignore null-valued keys) |
362
- | `isInstance(Class)` | `instanceof` check against given class |
224
+ `minDate(date)`, `maxDate(date)`
363
225
 
364
226
  ### Locale
365
227
 
366
- Locale-specific validators that accept a locale string parameter.
367
-
368
- | Rule | Description |
369
- |---|---|
370
- | `isMobilePhone(locale)` | Mobile phone number for the given locale (e.g. `'ko-KR'`, `'en-US'`, `'ja-JP'`) |
371
- | `isPostalCode(locale)` | Postal code for the given locale/country code (e.g. `'US'`, `'KR'`, `'GB'`) |
372
- | `isIdentityCard(locale)` | National identity card number for the given locale (e.g. `'KR'`, `'US'`, `'CN'`) |
373
- | `isPassportNumber(locale)` | Passport number for the given locale (e.g. `'US'`, `'KR'`, `'GB'`) |
374
-
375
- ---
228
+ `isMobilePhone(locale)`, `isPostalCode(locale)`, `isIdentityCard(locale)`, `isPassportNumber(locale)`
376
229
 
377
- ## Configuration
378
-
379
- Call `configure()` **before** the first `deserialize()`/`serialize()`:
230
+ ## Nested DTOs
380
231
 
381
232
  ```typescript
382
- import { configure } from '@zipbul/baker';
383
-
384
- configure({
385
- autoConvert: false, // Implicit type conversion ("123" -> 123)
386
- allowClassDefaults: false, // Use class default values for missing keys
387
- stopAtFirstError: false, // Stop at first error or collect all
388
- forbidUnknown: false, // Reject undeclared fields
389
- debug: false, // Emit field exclusion comments in generated code
390
- });
391
- ```
392
-
393
- `configure()` returns `{ warnings: string[] }` — if called after auto-seal, warnings describe which classes won't be affected.
394
-
395
- ---
396
-
397
- ## Error Handling
398
-
399
- When validation fails, `deserialize()` throws a `BakerValidationError`:
400
-
401
- ```typescript
402
- class BakerValidationError extends Error {
403
- readonly errors: BakerError[];
404
- readonly className: string;
233
+ class AddressDto {
234
+ @Field(isString) city!: string;
405
235
  }
406
236
 
407
- interface BakerError {
408
- readonly path: string; // 'user.address.city'
409
- readonly code: string; // 'isString', 'min', 'isEmail'
410
- readonly message?: string; // Custom message
411
- readonly context?: unknown; // Custom context
237
+ class UserDto {
238
+ @Field({ type: () => AddressDto }) address!: AddressDto;
239
+ @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
412
240
  }
413
241
  ```
414
242
 
415
- ---
416
-
417
- ## Nested Objects
418
-
419
- Use `type` option for nested DTO validation:
243
+ ## Collections
420
244
 
421
245
  ```typescript
422
- class AddressDto {
423
- @Field(isString)
424
- city!: string;
425
- }
426
-
427
246
  class UserDto {
428
- @Field({ type: () => AddressDto })
429
- address!: AddressDto;
430
-
431
- // Array of nested DTOs
432
- @Field({ type: () => [AddressDto] })
433
- addresses!: AddressDto[];
247
+ @Field({ type: () => Set as any, setValue: () => TagDto }) tags!: Set<TagDto>;
248
+ @Field({ type: () => Map as any, mapValue: () => PriceDto }) prices!: Map<string, PriceDto>;
434
249
  }
435
250
  ```
436
251
 
437
- ### Discriminator (Polymorphism)
252
+ ## Discriminator
438
253
 
439
254
  ```typescript
440
- class DogDto {
441
- @Field(isString) breed!: string;
442
- }
443
- class CatDto {
444
- @Field(isBoolean) indoor!: boolean;
445
- }
446
-
447
- class PetOwnerDto {
255
+ class PetOwner {
448
256
  @Field({
449
- type: () => DogDto,
257
+ type: () => CatDto,
450
258
  discriminator: {
451
- property: 'type',
259
+ property: 'kind',
452
260
  subTypes: [
453
- { value: DogDto, name: 'dog' },
454
261
  { value: CatDto, name: 'cat' },
262
+ { value: DogDto, name: 'dog' },
455
263
  ],
456
264
  },
457
- })
458
- pet!: DogDto | CatDto;
265
+ }) pet!: CatDto | DogDto;
459
266
  }
460
267
  ```
461
268
 
462
- Discriminator works in both directions — `deserialize()` switches on the property value, `serialize()` dispatches via `instanceof`.
463
-
464
- ### Map / Set Collections
465
-
466
- Baker auto-converts between `Map`/`Set` and JSON-compatible types:
467
-
468
- ```typescript
469
- // Set<primitive>: JSON array ↔ Set
470
- @Field({ type: () => Set })
471
- tags!: Set<string>;
472
-
473
- // Set<DTO>: JSON array of objects ↔ Set of DTO instances
474
- @Field({ type: () => Set, setValue: () => TagDto })
475
- tags!: Set<TagDto>;
476
-
477
- // Map<string, primitive>: JSON object ↔ Map
478
- @Field({ type: () => Map })
479
- config!: Map<string, unknown>;
480
-
481
- // Map<string, DTO>: JSON object ↔ Map of DTO instances
482
- @Field({ type: () => Map, mapValue: () => PriceDto })
483
- prices!: Map<string, PriceDto>;
484
- ```
485
-
486
- Map keys are always strings (JSON constraint). JSON Schema maps `Set` to `{ type: 'array', uniqueItems: true }` and `Map` to `{ type: 'object', additionalProperties: ... }`.
487
-
488
- ---
489
-
490
269
  ## Inheritance
491
270
 
492
- Baker supports class inheritance. Child DTOs automatically inherit all `@Field()` decorators from parent classes. You can override or extend fields in child classes:
493
-
494
271
  ```typescript
495
272
  class BaseDto {
496
- @Field(isString)
497
- name!: string;
498
- }
499
-
500
- class ExtendedDto extends BaseDto {
501
- @Field(isInt, min(0))
502
- age!: number;
503
- // `name` is inherited from BaseDto
504
- }
505
- ```
506
-
507
- ---
508
-
509
- ## Transform
510
-
511
- The `transform` option in `FieldOptions` lets you transform values during deserialization and/or serialization. Transform functions can be **async**.
512
-
513
- ```typescript
514
- class UserDto {
515
- @Field(isString, {
516
- transform: ({ value, direction }) => {
517
- return direction === 'deserialize'
518
- ? (value as string).trim().toLowerCase()
519
- : value;
520
- },
521
- })
522
- email!: string;
523
-
524
- @Field(isString, {
525
- transform: async ({ value }) => {
526
- return await someAsyncOperation(value);
527
- },
528
- transformDirection: 'deserializeOnly',
529
- })
530
- data!: string;
273
+ @Field(isString) id!: string;
531
274
  }
532
- ```
533
-
534
- ---
535
-
536
- ## Custom Rules
537
-
538
- ```typescript
539
- import { createRule } from '@zipbul/baker';
540
275
 
541
- const isPositiveInt = createRule({
542
- name: 'isPositiveInt',
543
- validate: (value) => Number.isInteger(value) && (value as number) > 0,
544
- });
545
-
546
- class Dto {
547
- @Field(isPositiveInt)
548
- count!: number;
276
+ class UserDto extends BaseDto {
277
+ @Field(isString) name!: string;
278
+ // inherits 'id' field with isString rule
549
279
  }
550
280
  ```
551
281
 
552
- ---
282
+ ## FAQ
553
283
 
554
- ## Class-level JSON Schema Metadata
284
+ ### When should I use baker instead of class-validator?
555
285
 
556
- Use `collectClassSchema()` to attach class-level JSON Schema metadata (title, description, etc.) to a DTO. This metadata is merged into the output of `toJsonSchema()`.
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.
557
287
 
558
- > `collectClassSchema` is a low-level API exported from `src/collect.ts`. It is not available as a subpath export and must be imported directly.
288
+ ### How does baker compare to Zod?
559
289
 
560
- ```typescript
561
- import { collectClassSchema } from '@zipbul/baker/src/collect';
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.
562
291
 
563
- class CreateUserDto {
564
- @Field(isString) name!: string;
565
- @Field(isEmail()) email!: string;
566
- }
292
+ ### Does baker support async validation?
567
293
 
568
- collectClassSchema(CreateUserDto, {
569
- title: 'CreateUserRequest',
570
- description: 'Payload for creating a new user',
571
- });
572
- ```
294
+ 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.
573
295
 
574
- For property-level schema overrides, use the `schema` option in `@Field()`:
296
+ ### Can I use baker with NestJS?
575
297
 
576
- ```typescript
577
- class Dto {
578
- @Field(isString, minLength(1), {
579
- schema: { description: 'User display name', minLength: 5 },
580
- })
581
- name!: string;
582
- }
583
- ```
298
+ Yes. baker's `@Field` decorator works alongside NestJS pipes. Use `deserialize()` in a custom validation pipe.
584
299
 
585
- ---
300
+ ### How does the AOT code generation work?
586
301
 
587
- ## JSON Schema
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.
588
303
 
589
- Generate JSON Schema Draft 2020-12 from your DTOs:
304
+ ## Exports
590
305
 
591
306
  ```typescript
592
- import { toJsonSchema } from '@zipbul/baker';
593
-
594
- const schema = toJsonSchema(CreateUserDto, {
595
- direction: 'deserialize', // 'deserialize' | 'serialize'
596
- groups: ['create'], // Filter by group
597
- onUnmappedRule: (name) => { /* custom rules without schema mapping */ },
598
- });
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';
309
+ import { isString, isEmail, isULID, isCUID2, ... } from '@zipbul/baker/rules';
310
+ import { trimTransformer, jsonTransformer, ... } from '@zipbul/baker/transformers';
599
311
  ```
600
312
 
601
- ---
602
-
603
- ## How It Works
604
-
605
- ```
606
- Decorators (@Field) auto-seal (first call) deserialize() / serialize()
607
- metadata -> new Function() codegen -> execute generated code
608
- ```
609
-
610
- 1. `@Field()` attaches validation metadata to class properties at definition time
611
- 2. First `deserialize()`/`serialize()` call triggers **auto-seal** — reads all metadata, analyzes circular references, generates optimized JavaScript functions via `new Function()`
612
- 3. Subsequent calls execute the generated function directly — no interpretation loops
613
-
614
- ---
615
-
616
- ## Subpath Exports
617
-
618
- | Import path | Purpose |
619
- |---|---|
620
- | `@zipbul/baker` | Main API: `deserialize`, `serialize`, `configure`, `toJsonSchema`, `Field`, `arrayOf`, `createRule` |
621
- | `@zipbul/baker/rules` | Rule functions and constants: `isString`, `min()`, `isEmail()`, `arrayOf()`, etc. |
622
-
623
- ---
624
-
625
313
  ## License
626
314
 
627
- [MIT](./LICENSE)
315
+ MIT