@zipbul/baker 0.1.2 → 1.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 (46) hide show
  1. package/README.ko.md +368 -280
  2. package/README.md +407 -280
  3. package/dist/index-70ggmxsa.js +6 -0
  4. package/dist/index-gcptd79v.js +6 -0
  5. package/dist/index-xdn55cz3.js +4 -0
  6. package/dist/index.d.ts +6 -5
  7. package/dist/index.js +211 -155
  8. package/dist/src/collect.d.ts +3 -3
  9. package/dist/src/configure.d.ts +33 -0
  10. package/dist/src/create-rule.d.ts +10 -19
  11. package/dist/src/decorators/field.d.ts +86 -0
  12. package/dist/src/decorators/index.d.ts +2 -14
  13. package/dist/src/decorators/index.js +2 -2
  14. package/dist/src/errors.d.ts +17 -17
  15. package/dist/src/functions/deserialize.d.ts +6 -4
  16. package/dist/src/functions/serialize.d.ts +5 -4
  17. package/dist/src/functions/to-json-schema.d.ts +11 -5
  18. package/dist/src/interfaces.d.ts +9 -30
  19. package/dist/src/registry.d.ts +4 -12
  20. package/dist/src/rules/index.d.ts +2 -0
  21. package/dist/src/rules/index.js +11 -2
  22. package/dist/src/rules/object.d.ts +1 -1
  23. package/dist/src/seal/circular-analyzer.d.ts +5 -9
  24. package/dist/src/seal/expose-validator.d.ts +6 -6
  25. package/dist/src/seal/index.d.ts +1 -1
  26. package/dist/src/seal/seal.d.ts +30 -15
  27. package/dist/src/seal/serialize-builder.d.ts +2 -2
  28. package/dist/src/symbols.d.ts +5 -5
  29. package/dist/src/symbols.js +2 -2
  30. package/dist/src/types.d.ts +38 -32
  31. package/dist/src/utils.d.ts +2 -0
  32. package/package.json +1 -1
  33. package/dist/index-3gcf6hkv.js +0 -5
  34. package/dist/index-mx6gnk4h.js +0 -6
  35. package/dist/index-wy5sh2nx.js +0 -15
  36. package/dist/src/decorators/array.d.ts +0 -13
  37. package/dist/src/decorators/common.d.ts +0 -39
  38. package/dist/src/decorators/date.d.ts +0 -5
  39. package/dist/src/decorators/locales.d.ts +0 -9
  40. package/dist/src/decorators/nested.d.ts +0 -17
  41. package/dist/src/decorators/number.d.ts +0 -15
  42. package/dist/src/decorators/object.d.ts +0 -9
  43. package/dist/src/decorators/schema.d.ts +0 -13
  44. package/dist/src/decorators/string.d.ts +0 -72
  45. package/dist/src/decorators/transform.d.ts +0 -68
  46. package/dist/src/decorators/typechecker.d.ts +0 -18
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <strong>Decorator-based validate + transform with inline code generation</strong>
5
5
  </p>
6
6
  <p align="center">
7
- class-validator DX · AOT-level performance · zero reflect-metadata
7
+ Single <code>@Field()</code> decorator &middot; AOT-level performance &middot; zero reflect-metadata
8
8
  </p>
9
9
  <p align="center">
10
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>
@@ -20,42 +20,47 @@
20
20
 
21
21
  ---
22
22
 
23
- ## 🤔 Why Baker?
23
+ ## Why Baker?
24
24
 
25
25
  | | class-validator | Zod | TypeBox | **Baker** |
26
26
  |---|---|---|---|---|
27
- | Schema style | Decorators | Function chaining | JSON Schema builder | **Decorators** |
27
+ | Schema style | Decorators | Function chaining | JSON Schema builder | **Single `@Field()` decorator** |
28
28
  | Performance | Runtime interpreter | Runtime interpreter | JIT compile | **`new Function()` inline codegen** |
29
- | Transform built-in | Separate package | `.transform()` | | **Unified** |
29
+ | Transform built-in | Separate package | `.transform()` | N/A | **Unified** |
30
30
  | reflect-metadata | Required | N/A | N/A | **Not needed** |
31
31
  | class-validator migration | — | Full rewrite | Full rewrite | **Near drop-in** |
32
32
 
33
- Baker gives you the **familiar decorator DX** of class-validator while generating optimized validation + transformation functions via `new Function()` at seal time — delivering **AOT-equivalent performance without a compiler plugin**.
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
34
 
35
35
  ---
36
36
 
37
- ## Features
38
-
39
- - 🎯 **Decorator-first** — `@IsString()`, `@Min()`, `@IsEmail()` and 80+ built-in validators
40
- - **Inline code generation** — `seal()` compiles validators into optimized functions, no runtime interpretation
41
- - 🔄 **Unified validate + transform** — `deserialize()` and `serialize()` in one async call
42
- - 🪶 **Zero reflect-metadata** — no `reflect-metadata` import needed
43
- - 🔁 **Circular reference detection** — automatic static analysis at seal time
44
- - 🏷️ **Group-based validation** — apply different rules per request with `groups`
45
- - 🧩 **Custom rules** — `createRule()` for user-defined validators with codegen support
46
- - 📐 **JSON Schema output** — `toJsonSchema()` generates JSON Schema Draft 2020-12 from your DTOs
47
- - 🪆 **`@Nested`**single-decorator nested DTO validation with discriminator support
48
- - 🛡️ **Whitelist mode** — reject undeclared fields with `seal({ whitelist: true })`
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
49
54
 
50
55
  ---
51
56
 
52
- ## 📦 Installation
57
+ ## Installation
53
58
 
54
59
  ```bash
55
60
  bun add @zipbul/baker
56
61
  ```
57
62
 
58
- > **Requirements:** Bun 1.0, `experimentalDecorators: true` in tsconfig.json
63
+ > **Requirements:** Bun >= 1.0, `experimentalDecorators: true` in tsconfig.json
59
64
 
60
65
  ```jsonc
61
66
  // tsconfig.json
@@ -68,37 +73,27 @@ bun add @zipbul/baker
68
73
 
69
74
  ---
70
75
 
71
- ## 🚀 Quick Start
76
+ ## Quick Start
72
77
 
73
78
  ### 1. Define a DTO
74
79
 
75
80
  ```typescript
76
- import { IsString, IsInt, IsEmail, Min, Max } from '@zipbul/baker/decorators';
81
+ import { Field } from '@zipbul/baker';
82
+ import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
77
83
 
78
84
  class CreateUserDto {
79
- @IsString()
85
+ @Field(isString)
80
86
  name!: string;
81
87
 
82
- @IsInt()
83
- @Min(0)
84
- @Max(120)
88
+ @Field(isInt, min(0), max(120))
85
89
  age!: number;
86
90
 
87
- @IsEmail()
91
+ @Field(isEmail())
88
92
  email!: string;
89
93
  }
90
94
  ```
91
95
 
92
- ### 2. Seal at startup
93
-
94
- ```typescript
95
- import { seal } from '@zipbul/baker';
96
-
97
- // Compiles all registered DTOs into optimized validators
98
- seal();
99
- ```
100
-
101
- ### 3. Deserialize per request
96
+ ### 2. Deserialize (auto-seals on first call)
102
97
 
103
98
  ```typescript
104
99
  import { deserialize, BakerValidationError } from '@zipbul/baker';
@@ -113,7 +108,7 @@ try {
113
108
  }
114
109
  ```
115
110
 
116
- ### 4. Serialize
111
+ ### 3. Serialize
117
112
 
118
113
  ```typescript
119
114
  import { serialize } from '@zipbul/baker';
@@ -122,180 +117,284 @@ const plain = await serialize(userInstance);
122
117
  // plain: Record<string, unknown>
123
118
  ```
124
119
 
120
+ > No `seal()` call needed — baker auto-seals all registered DTOs on the first `deserialize()` or `serialize()` call.
121
+
125
122
  ---
126
123
 
127
- ## 🏗️ Decorators
124
+ ## The `@Field()` Decorator
125
+
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.
127
+
128
+ ### Signatures
129
+
130
+ ```typescript
131
+ // Rules only
132
+ @Field(isString, minLength(3), maxLength(100))
133
+
134
+ // Options only
135
+ @Field({ optional: true, nullable: true })
136
+
137
+ // Rules + options
138
+ @Field(isString, { name: 'user_name', groups: ['create'] })
139
+
140
+ // No rules (plain field)
141
+ @Field()
142
+ ```
143
+
144
+ ### FieldOptions
145
+
146
+ ```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
+ }
171
+ ```
172
+
173
+ ### Per-field Error Messages
174
+
175
+ Use `message` and `context` to customize validation error output:
176
+
177
+ ```typescript
178
+ @Field(isString, minLength(3), { message: 'Name is invalid' })
179
+ name!: string;
180
+
181
+ @Field(isEmail(), {
182
+ message: ({ property, value }) => `${property} got bad value: ${value}`,
183
+ context: { severity: 'error' },
184
+ })
185
+ email!: string;
186
+ ```
187
+
188
+ The `message` and `context` are applied to all rules on the field. They appear in `BakerError.message` and `BakerError.context` on validation failure.
189
+
190
+ ### `arrayOf()` — Array Element Validation
191
+
192
+ `arrayOf()` applies rules to each element of an array. Import it from `@zipbul/baker/rules` or `@zipbul/baker`.
193
+
194
+ ```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
+ }
202
+ ```
203
+
204
+ You can mix `arrayOf()` with top-level array rules:
205
+
206
+ ```typescript
207
+ import { arrayMinSize, arrayMaxSize } from '@zipbul/baker/rules';
208
+
209
+ class ScoresDto {
210
+ @Field(arrayMinSize(1), arrayMaxSize(10), arrayOf(isInt, min(0), max(100)))
211
+ scores!: number[];
212
+ }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Built-in Rules
218
+
219
+ All rules are imported from `@zipbul/baker/rules` and passed as arguments to `@Field()`.
220
+
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.
128
222
 
129
223
  ### Type Checkers
130
224
 
131
- | Decorator | Description |
225
+ | Rule | Description |
132
226
  |---|---|
133
- | `@IsString()` | `typeof === 'string'` |
134
- | `@IsNumber(opts?)` | `typeof === 'number'` with NaN/Infinity checks |
135
- | `@IsInt()` | Integer check |
136
- | `@IsBoolean()` | `typeof === 'boolean'` |
137
- | `@IsDate()` | `instanceof Date && !isNaN` |
138
- | `@IsEnum(enumObj)` | Enum value check |
139
- | `@IsArray()` | `Array.isArray()` |
140
- | `@IsObject()` | `typeof === 'object'`, excludes null/Array |
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 |
235
+
236
+ > `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` are constants (no parentheses needed). `isNumber(opts?)` and `isEnum(enumObj)` are factory functions.
141
237
 
142
238
  ### Common
143
239
 
144
- | Decorator | Description |
240
+ | Rule | Description |
145
241
  |---|---|
146
- | `@IsDefined()` | `!== undefined && !== null` |
147
- | `@IsOptional()` | Skip subsequent rules if value is absent |
148
- | `@IsNullable()` | Allow `null` (skip validation), reject `undefined` |
149
- | `@IsNotEmpty()` | `!== undefined && !== null && !== ''` |
150
- | `@IsEmpty()` | `=== undefined \|\| === null \|\| === ''` |
151
- | `@Equals(val)` | `=== val` |
152
- | `@NotEquals(val)` | `!== val` |
153
- | `@IsIn(values)` | Value is in the given array |
154
- | `@IsNotIn(values)` | Value is not in the given array |
155
- | `@ValidateNested()` | Validate nested DTO |
156
- | `@ValidateIf(fn)` | Conditional validation |
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 |
248
+
249
+ > `isEmpty` and `isNotEmpty` are constants. The rest are factory functions.
157
250
 
158
251
  ### Number
159
252
 
160
- | Decorator | Description |
253
+ | Rule | Description |
161
254
  |---|---|
162
- | `@Min(n)` | `value >= n` |
163
- | `@Max(n)` | `value <= n` |
164
- | `@IsPositive()` | `value > 0` |
165
- | `@IsNegative()` | `value < 0` |
166
- | `@IsInRange(min, max)` | `min <= value <= max` |
167
- | `@IsDivisibleBy(n)` | `value % n === 0` |
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` |
260
+
261
+ > `isPositive` and `isNegative` are constants (no parentheses). `min()`, `max()`, and `isDivisibleBy()` are factory functions.
168
262
 
169
263
  ### String
170
264
 
171
- <details>
172
- <summary>50+ string validators — click to expand</summary>
265
+ All string rules require the value to be a `string` type.
266
+
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 |
173
338
 
174
- | Decorator | Description |
339
+ ### Array
340
+
341
+ | Rule | Description |
175
342
  |---|---|
176
- | `@MinLength(n)` | Minimum length |
177
- | `@MaxLength(n)` | Maximum length |
178
- | `@Length(min, max)` | Length range |
179
- | `@Contains(seed)` | Contains substring |
180
- | `@NotContains(seed)` | Does not contain substring |
181
- | `@Matches(pattern)` | Regex match |
182
- | `@IsAlpha()` | Alphabetic only |
183
- | `@IsAlphanumeric()` | Alphanumeric only |
184
- | `@IsNumeric()` | Numeric string |
185
- | `@IsEmail(opts?)` | Email format |
186
- | `@IsURL(opts?)` | URL format |
187
- | `@IsUUID(version?)` | UUID v1–v5 |
188
- | `@IsIP(version?)` | IPv4 / IPv6 |
189
- | `@IsMACAddress()` | MAC address |
190
- | `@IsISBN(version?)` | ISBN-10 / ISBN-13 |
191
- | `@IsISIN()` | ISIN |
192
- | `@IsIBAN()` | IBAN |
193
- | `@IsJSON()` | Parseable JSON string |
194
- | `@IsBase64()` | Base64 encoded |
195
- | `@IsBase32()` | Base32 encoded |
196
- | `@IsBase58()` | Base58 encoded |
197
- | `@IsHexColor()` | Hex color code |
198
- | `@IsHSL()` | HSL color |
199
- | `@IsRgbColor()` | RGB color |
200
- | `@IsHexadecimal()` | Hex string |
201
- | `@IsBIC()` | BIC/SWIFT code |
202
- | `@IsISRC()` | ISRC code |
203
- | `@IsEAN()` | EAN barcode |
204
- | `@IsMimeType()` | MIME type |
205
- | `@IsMagnetURI()` | Magnet URI |
206
- | `@IsCreditCard()` | Credit card number |
207
- | `@IsHash(algorithm)` | Hash (`md5 \| sha1 \| sha256 \| sha512` etc.) |
208
- | `@IsRFC3339()` | RFC 3339 date |
209
- | `@IsMilitaryTime()` | 24h format (`HH:MM`) |
210
- | `@IsLatitude()` | Latitude (-90 ~ 90) |
211
- | `@IsLongitude()` | Longitude (-180 ~ 180) |
212
- | `@IsEthereumAddress()` | Ethereum address |
213
- | `@IsBtcAddress()` | Bitcoin address (P2PKH/P2SH/bech32) |
214
- | `@IsISO4217CurrencyCode()` | ISO 4217 currency code |
215
- | `@IsPhoneNumber()` | E.164 international phone number |
216
- | `@IsStrongPassword(opts?)` | Strong password |
217
- | `@IsSemVer()` | Semantic version |
218
- | `@IsISO8601()` | ISO 8601 date string |
219
- | `@IsMongoId()` | MongoDB ObjectId |
220
- | `@IsTaxId(locale)` | Tax ID by locale |
221
-
222
- </details>
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 |
223
349
 
224
350
  ### Date
225
351
 
226
- | Decorator | Description |
352
+ | Rule | Description |
227
353
  |---|---|
228
- | `@MinDate(date)` | Minimum date |
229
- | `@MaxDate(date)` | Maximum date |
354
+ | `minDate(date)` | Minimum date |
355
+ | `maxDate(date)` | Maximum date |
230
356
 
231
- ### Array
357
+ ### Object
232
358
 
233
- | Decorator | Description |
359
+ | Rule | Description |
234
360
  |---|---|
235
- | `@ArrayContains(values)` | Contains all given elements |
236
- | `@ArrayNotContains(values)` | Contains none of the given elements |
237
- | `@ArrayMinSize(n)` | Minimum array length |
238
- | `@ArrayMaxSize(n)` | Maximum array length |
239
- | `@ArrayUnique()` | No duplicates |
240
- | `@ArrayNotEmpty()` | Not empty |
361
+ | `isNotEmptyObject(opts?)` | At least one key (supports `{ nullable: true }` to ignore null-valued keys) |
362
+ | `isInstance(Class)` | `instanceof` check against given class |
241
363
 
242
- ### Locale-specific
243
-
244
- | Decorator | Description |
245
- |---|---|
246
- | `@IsMobilePhone(locale)` | Mobile phone by locale |
247
- | `@IsPostalCode(locale)` | Postal code by locale |
248
- | `@IsIdentityCard(locale)` | Identity card by locale |
249
- | `@IsPassportNumber(locale)` | Passport number by locale |
364
+ ### Locale
250
365
 
251
- ### Transform & Type
366
+ Locale-specific validators that accept a locale string parameter.
252
367
 
253
- | Decorator | Description |
368
+ | Rule | Description |
254
369
  |---|---|
255
- | `@Transform(fn, opts?)` | Custom transform function |
256
- | `@Type(fn)` | Nested DTO type + implicit conversion |
257
- | `@Nested(fn, opts?)` | Shorthand for `@ValidateNested()` + `@Type(fn)` with discriminator support |
258
- | `@Expose(opts?)` | Control property exposure |
259
- | `@Exclude(opts?)` | Exclude property from serialization |
260
- | `@Schema(schema)` | Attach JSON Schema metadata (class or property level) |
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'`) |
261
374
 
262
375
  ---
263
376
 
264
- ## ⚙️ Validation Options
377
+ ## Configuration
265
378
 
266
- Every validation decorator accepts `ValidationOptions` as its last argument:
379
+ Call `configure()` **before** the first `deserialize()`/`serialize()`:
267
380
 
268
381
  ```typescript
269
- interface ValidationOptions {
270
- each?: boolean; // Apply rule to each array element
271
- groups?: string[]; // Groups this rule belongs to
272
- message?: string | ((args: {
273
- property: string;
274
- value: unknown;
275
- constraints: Record<string, unknown>;
276
- }) => string); // Custom error message
277
- context?: unknown; // Arbitrary context attached to error
278
- }
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
+ });
279
391
  ```
280
392
 
281
- **Example:**
282
-
283
- ```typescript
284
- class UserDto {
285
- @IsString({ message: 'Name must be a string' })
286
- name!: string;
287
-
288
- @IsInt({
289
- message: ({ property }) => `${property} must be an integer`,
290
- context: { httpStatus: 400 },
291
- })
292
- age!: number;
293
- }
294
- ```
393
+ `configure()` returns `{ warnings: string[] }` — if called after auto-seal, warnings describe which classes won't be affected.
295
394
 
296
395
  ---
297
396
 
298
- ## 🚨 Error Handling
397
+ ## Error Handling
299
398
 
300
399
  When validation fails, `deserialize()` throws a `BakerValidationError`:
301
400
 
@@ -304,86 +403,50 @@ class BakerValidationError extends Error {
304
403
  readonly errors: BakerError[];
305
404
  readonly className: string;
306
405
  }
307
- ```
308
406
 
309
- Each error follows the `BakerError` interface:
310
-
311
- ```typescript
312
407
  interface BakerError {
313
- readonly path: string; // Field path ('user.address.city')
314
- readonly code: string; // Error code ('isString', 'min', 'isEmail')
315
- readonly message?: string; // Custom message (when message option is set)
316
- readonly context?: unknown; // Custom context (when context option is set)
317
- }
318
- ```
319
-
320
- ---
321
-
322
- ## 📋 Array Validation
323
-
324
- Use `each: true` to apply rules to each element of an Array, Set, or Map:
325
-
326
- ```typescript
327
- class TagsDto {
328
- @IsString({ each: true })
329
- tags!: string[];
330
- }
331
- ```
332
-
333
- ---
334
-
335
- ## 🏷️ Group-based Validation
336
-
337
- Apply different rules depending on the use case:
338
-
339
- ```typescript
340
- class UserDto {
341
- @IsString({ groups: ['create'] })
342
- name!: string;
343
-
344
- @IsEmail({ groups: ['create', 'update'] })
345
- email!: string;
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
346
412
  }
347
-
348
- // Only validate rules in the 'create' group
349
- const user = await deserialize(UserDto, body, { groups: ['create'] });
350
413
  ```
351
414
 
352
415
  ---
353
416
 
354
- ## 🪆 Nested Objects
417
+ ## Nested Objects
355
418
 
356
- Use `@Nested` for nested DTO validation:
419
+ Use `type` option for nested DTO validation:
357
420
 
358
421
  ```typescript
359
- import { Nested, IsString } from '@zipbul/baker';
360
-
361
422
  class AddressDto {
362
- @IsString()
423
+ @Field(isString)
363
424
  city!: string;
364
425
  }
365
426
 
366
427
  class UserDto {
367
- @Nested(() => AddressDto)
428
+ @Field({ type: () => AddressDto })
368
429
  address!: AddressDto;
430
+
431
+ // Array of nested DTOs
432
+ @Field({ type: () => [AddressDto] })
433
+ addresses!: AddressDto[];
369
434
  }
370
435
  ```
371
436
 
372
- Array nesting and discriminator polymorphism:
437
+ ### Discriminator (Polymorphism)
373
438
 
374
439
  ```typescript
375
- class ItemDto {
376
- @IsString()
377
- label!: string;
440
+ class DogDto {
441
+ @Field(isString) breed!: string;
378
442
  }
379
-
380
- class ListDto {
381
- @Nested(() => ItemDto, { each: true })
382
- items!: ItemDto[];
443
+ class CatDto {
444
+ @Field(isBoolean) indoor!: boolean;
383
445
  }
384
446
 
385
447
  class PetOwnerDto {
386
- @Nested(() => DogDto, {
448
+ @Field({
449
+ type: () => DogDto,
387
450
  discriminator: {
388
451
  property: 'type',
389
452
  subTypes: [
@@ -396,105 +459,169 @@ class PetOwnerDto {
396
459
  }
397
460
  ```
398
461
 
399
- ---
462
+ Discriminator works in both directions — `deserialize()` switches on the property value, `serialize()` dispatches via `instanceof`.
400
463
 
401
- ## 🧩 Custom Rules
464
+ ### Map / Set Collections
402
465
 
403
- Create user-defined validation rules with codegen support:
466
+ Baker auto-converts between `Map`/`Set` and JSON-compatible types:
404
467
 
405
468
  ```typescript
406
- import { createRule } from '@zipbul/baker';
469
+ // Set<primitive>: JSON array Set
470
+ @Field({ type: () => Set })
471
+ tags!: Set<string>;
407
472
 
408
- const isPositiveInt = createRule({
409
- name: 'isPositiveInt',
410
- validate: (value) => Number.isInteger(value) && (value as number) > 0,
411
- emit: (varName, ctx) =>
412
- `if (!Number.isInteger(${varName}) || ${varName} <= 0) ${ctx.fail('isPositiveInt')};`,
413
- });
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>;
414
484
  ```
415
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
+
416
488
  ---
417
489
 
418
- ## ⚙️ Seal Options
490
+ ## Inheritance
491
+
492
+ Baker supports class inheritance. Child DTOs automatically inherit all `@Field()` decorators from parent classes. You can override or extend fields in child classes:
419
493
 
420
494
  ```typescript
421
- seal({
422
- enableImplicitConversion: false, // Auto-convert types based on decorators
423
- enableCircularCheck: 'auto', // Detect circular references ('auto' | true | false)
424
- exposeDefaultValues: false, // Use class defaults for missing keys
425
- stopAtFirstError: false, // Stop at first error or collect all
426
- whitelist: false, // Reject undeclared fields
427
- debug: false, // Store generated source for inspection
428
- });
495
+ 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
+ }
429
505
  ```
430
506
 
431
507
  ---
432
508
 
433
- ## 📐 JSON Schema
509
+ ## Transform
434
510
 
435
- Generate JSON Schema Draft 2020-12 from your DTOs:
511
+ The `transform` option in `FieldOptions` lets you transform values during deserialization and/or serialization. Transform functions can be **async**.
436
512
 
437
513
  ```typescript
438
- import { toJsonSchema } from '@zipbul/baker';
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;
439
523
 
440
- const schema = toJsonSchema(CreateUserDto);
441
- // { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { ... } }
524
+ @Field(isString, {
525
+ transform: async ({ value }) => {
526
+ return await someAsyncOperation(value);
527
+ },
528
+ transformDirection: 'deserializeOnly',
529
+ })
530
+ data!: string;
531
+ }
442
532
  ```
443
533
 
444
- Options:
534
+ ---
535
+
536
+ ## Custom Rules
445
537
 
446
538
  ```typescript
447
- toJsonSchema(CreateUserDto, {
448
- direction: 'deserialize', // 'deserialize' | 'serialize' — filters @Expose/@Exclude direction
449
- groups: ['create'], // Filter rules/fields by group
539
+ import { createRule } from '@zipbul/baker';
540
+
541
+ const isPositiveInt = createRule({
542
+ name: 'isPositiveInt',
543
+ validate: (value) => Number.isInteger(value) && (value as number) > 0,
450
544
  });
545
+
546
+ class Dto {
547
+ @Field(isPositiveInt)
548
+ count!: number;
549
+ }
451
550
  ```
452
551
 
453
- Use `@Schema()` to attach additional JSON Schema metadata:
552
+ ---
553
+
554
+ ## Class-level JSON Schema Metadata
555
+
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()`.
557
+
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.
454
559
 
455
560
  ```typescript
456
- @Schema({ title: 'CreateUser', description: 'Creates a new user' })
561
+ import { collectClassSchema } from '@zipbul/baker/src/collect';
562
+
457
563
  class CreateUserDto {
458
- @IsString()
459
- @Schema({ description: 'Display name', examples: ['Alice'] })
564
+ @Field(isString) name!: string;
565
+ @Field(isEmail()) email!: string;
566
+ }
567
+
568
+ collectClassSchema(CreateUserDto, {
569
+ title: 'CreateUserRequest',
570
+ description: 'Payload for creating a new user',
571
+ });
572
+ ```
573
+
574
+ For property-level schema overrides, use the `schema` option in `@Field()`:
575
+
576
+ ```typescript
577
+ class Dto {
578
+ @Field(isString, minLength(1), {
579
+ schema: { description: 'User display name', minLength: 5 },
580
+ })
460
581
  name!: string;
461
582
  }
462
583
  ```
463
584
 
464
585
  ---
465
586
 
466
- ## 📂 Subpath Exports
587
+ ## JSON Schema
467
588
 
468
- | Import path | Purpose |
469
- |---|---|
470
- | `@zipbul/baker` | Main API: `seal`, `deserialize`, `serialize`, `toJsonSchema`, all decorators |
471
- | `@zipbul/baker/decorators` | Decorators only |
472
- | `@zipbul/baker/rules` | Raw rule objects |
473
- | `@zipbul/baker/symbols` | Internal symbols |
589
+ Generate JSON Schema Draft 2020-12 from your DTOs:
590
+
591
+ ```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
+ });
599
+ ```
474
600
 
475
601
  ---
476
602
 
477
- ## 🔍 How It Works
603
+ ## How It Works
478
604
 
479
605
  ```
480
- ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
481
- │ Decorators │ ──▶ │ seal() │ ──▶ │ new Function() code
482
- │ (metadata) │ │ at startup │ │ (inline codegen) │
483
- └─────────────┘ └──────────────┘ └──────────┬──────────┘
484
-
485
- ┌──────────▼──────────┐
486
- │ deserialize() / │
487
- │ serialize() │
488
- │ (execute generated) │
489
- └─────────────────────┘
606
+ Decorators (@Field) auto-seal (first call) deserialize() / serialize()
607
+ metadata -> new Function() codegen -> execute generated code
490
608
  ```
491
609
 
492
- 1. **Decorators** attach validation metadata to class properties at definition time
493
- 2. **`seal()`** reads all metadata, analyzes circular references, and generates inline JavaScript functions via `new Function()`
494
- 3. **`deserialize()` / `serialize()`** execute the generated function — no interpretation loops, just straight-line optimized code
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. |
495
622
 
496
623
  ---
497
624
 
498
- ## 📄 License
625
+ ## License
499
626
 
500
- [MIT](./LICENSE) © [Junhyung Park](https://github.com/parkrevil)
627
+ [MIT](./LICENSE)