@zipbul/baker 0.1.2 → 1.0.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 (40) hide show
  1. package/README.ko.md +368 -280
  2. package/README.md +369 -281
  3. package/dist/index-57gr0v18.js +6 -0
  4. package/dist/index-aegrb1kn.js +6 -0
  5. package/dist/index-xdn55cz3.js +4 -0
  6. package/dist/index.d.ts +6 -5
  7. package/dist/index.js +151 -152
  8. package/dist/src/configure.d.ts +33 -0
  9. package/dist/src/create-rule.d.ts +6 -15
  10. package/dist/src/decorators/field.d.ts +74 -0
  11. package/dist/src/decorators/index.d.ts +2 -14
  12. package/dist/src/decorators/index.js +2 -2
  13. package/dist/src/functions/deserialize.d.ts +2 -1
  14. package/dist/src/functions/serialize.d.ts +2 -2
  15. package/dist/src/functions/to-json-schema.d.ts +6 -0
  16. package/dist/src/interfaces.d.ts +5 -26
  17. package/dist/src/registry.d.ts +0 -8
  18. package/dist/src/rules/index.d.ts +2 -0
  19. package/dist/src/rules/index.js +11 -2
  20. package/dist/src/seal/circular-analyzer.d.ts +2 -6
  21. package/dist/src/seal/index.d.ts +1 -1
  22. package/dist/src/seal/seal.d.ts +21 -6
  23. package/dist/src/symbols.js +2 -2
  24. package/dist/src/types.d.ts +7 -7
  25. package/dist/src/utils.d.ts +2 -0
  26. package/package.json +1 -1
  27. package/dist/index-3gcf6hkv.js +0 -5
  28. package/dist/index-mx6gnk4h.js +0 -6
  29. package/dist/index-wy5sh2nx.js +0 -15
  30. package/dist/src/decorators/array.d.ts +0 -13
  31. package/dist/src/decorators/common.d.ts +0 -39
  32. package/dist/src/decorators/date.d.ts +0 -5
  33. package/dist/src/decorators/locales.d.ts +0 -9
  34. package/dist/src/decorators/nested.d.ts +0 -17
  35. package/dist/src/decorators/number.d.ts +0 -15
  36. package/dist/src/decorators/object.d.ts +0 -9
  37. package/dist/src/decorators/schema.d.ts +0 -13
  38. package/dist/src/decorators/string.d.ts +0 -72
  39. package/dist/src/decorators/transform.d.ts +0 -68
  40. 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,45 @@
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 async call
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
49
52
 
50
53
  ---
51
54
 
52
- ## 📦 Installation
55
+ ## Installation
53
56
 
54
57
  ```bash
55
58
  bun add @zipbul/baker
56
59
  ```
57
60
 
58
- > **Requirements:** Bun 1.0, `experimentalDecorators: true` in tsconfig.json
61
+ > **Requirements:** Bun >= 1.0, `experimentalDecorators: true` in tsconfig.json
59
62
 
60
63
  ```jsonc
61
64
  // tsconfig.json
@@ -68,37 +71,27 @@ bun add @zipbul/baker
68
71
 
69
72
  ---
70
73
 
71
- ## 🚀 Quick Start
74
+ ## Quick Start
72
75
 
73
76
  ### 1. Define a DTO
74
77
 
75
78
  ```typescript
76
- import { IsString, IsInt, IsEmail, Min, Max } from '@zipbul/baker/decorators';
79
+ import { Field } from '@zipbul/baker';
80
+ import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
77
81
 
78
82
  class CreateUserDto {
79
- @IsString()
83
+ @Field(isString)
80
84
  name!: string;
81
85
 
82
- @IsInt()
83
- @Min(0)
84
- @Max(120)
86
+ @Field(isInt, min(0), max(120))
85
87
  age!: number;
86
88
 
87
- @IsEmail()
89
+ @Field(isEmail())
88
90
  email!: string;
89
91
  }
90
92
  ```
91
93
 
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
94
+ ### 2. Deserialize (auto-seals on first call)
102
95
 
103
96
  ```typescript
104
97
  import { deserialize, BakerValidationError } from '@zipbul/baker';
@@ -113,7 +106,7 @@ try {
113
106
  }
114
107
  ```
115
108
 
116
- ### 4. Serialize
109
+ ### 3. Serialize
117
110
 
118
111
  ```typescript
119
112
  import { serialize } from '@zipbul/baker';
@@ -122,180 +115,271 @@ const plain = await serialize(userInstance);
122
115
  // plain: Record<string, unknown>
123
116
  ```
124
117
 
118
+ > No `seal()` call needed — baker auto-seals all registered DTOs on the first `deserialize()` or `serialize()` call.
119
+
120
+ ---
121
+
122
+ ## The `@Field()` Decorator
123
+
124
+ `@Field()` is the single decorator that replaces all individual decorators. It accepts validation rules as positional arguments and an options object for advanced features.
125
+
126
+ ### Signatures
127
+
128
+ ```typescript
129
+ // Rules only
130
+ @Field(isString, minLength(3), maxLength(100))
131
+
132
+ // Options only
133
+ @Field({ optional: true, nullable: true })
134
+
135
+ // Rules + options
136
+ @Field(isString, { name: 'user_name', groups: ['create'] })
137
+
138
+ // No rules (plain field)
139
+ @Field()
140
+ ```
141
+
142
+ ### FieldOptions
143
+
144
+ ```typescript
145
+ interface FieldOptions {
146
+ type?: () => Constructor | [Constructor]; // Nested DTO type (thunk for circular refs)
147
+ discriminator?: { // Polymorphic union
148
+ property: string;
149
+ subTypes: { value: Function; name: string }[];
150
+ };
151
+ keepDiscriminatorProperty?: boolean; // Keep discriminator key in output
152
+ rules?: (EmittableRule | ArrayOfMarker)[]; // Validation rules (alternative to positional args)
153
+ optional?: boolean; // Allow undefined
154
+ nullable?: boolean; // Allow null
155
+ name?: string; // JSON key mapping (bidirectional)
156
+ deserializeName?: string; // Deserialize-only key mapping
157
+ serializeName?: string; // Serialize-only key mapping
158
+ exclude?: boolean | 'deserializeOnly' | 'serializeOnly';
159
+ groups?: string[]; // Visibility + conditional validation
160
+ when?: (obj: any) => boolean; // Conditional validation
161
+ schema?: JsonSchemaOverride; // JSON Schema metadata
162
+ transform?: (params: FieldTransformParams) => unknown;
163
+ transformDirection?: 'deserializeOnly' | 'serializeOnly';
164
+ }
165
+ ```
166
+
167
+ ### Per-rule Options (message, groups)
168
+
169
+ Per-rule options like `message`, `groups`, and `context` are **not** passed as arguments to individual rule functions. Instead, they are controlled at the `@Field()` level:
170
+
171
+ - **`groups`** — set via `FieldOptions.groups` (applies to all rules on that field)
172
+ - **`message`** / **`context`** — use `createRule()` for custom error messages, or handle via `BakerError.code`
173
+ - **`each` (array element validation)** — use `arrayOf()` (see below)
174
+
175
+ ### `arrayOf()` — Array Element Validation
176
+
177
+ `arrayOf()` applies rules to each element of an array. Import it from `@zipbul/baker/rules` or `@zipbul/baker`.
178
+
179
+ ```typescript
180
+ import { Field, arrayOf } from '@zipbul/baker';
181
+ import { isString, minLength } from '@zipbul/baker/rules';
182
+
183
+ class TagsDto {
184
+ @Field(arrayOf(isString, minLength(1)))
185
+ tags!: string[];
186
+ }
187
+ ```
188
+
189
+ You can mix `arrayOf()` with top-level array rules:
190
+
191
+ ```typescript
192
+ import { arrayMinSize, arrayMaxSize } from '@zipbul/baker/rules';
193
+
194
+ class ScoresDto {
195
+ @Field(arrayMinSize(1), arrayMaxSize(10), arrayOf(isInt, min(0), max(100)))
196
+ scores!: number[];
197
+ }
198
+ ```
199
+
125
200
  ---
126
201
 
127
- ## 🏗️ Decorators
202
+ ## Built-in Rules
203
+
204
+ All rules are imported from `@zipbul/baker/rules` and passed as arguments to `@Field()`.
205
+
206
+ > **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
207
 
129
208
  ### Type Checkers
130
209
 
131
- | Decorator | Description |
210
+ | Rule | Description |
132
211
  |---|---|
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 |
212
+ | `isString` | `typeof === 'string'` |
213
+ | `isNumber(opts?)` | `typeof === 'number'` with NaN/Infinity/maxDecimalPlaces checks |
214
+ | `isInt` | Integer check |
215
+ | `isBoolean` | `typeof === 'boolean'` |
216
+ | `isDate` | `instanceof Date && !isNaN` |
217
+ | `isEnum(enumObj)` | Enum value check |
218
+ | `isArray` | `Array.isArray()` |
219
+ | `isObject` | `typeof === 'object'`, excludes null/Array |
220
+
221
+ > `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` are constants (no parentheses needed). `isNumber(opts?)` and `isEnum(enumObj)` are factory functions.
141
222
 
142
223
  ### Common
143
224
 
144
- | Decorator | Description |
225
+ | Rule | Description |
145
226
  |---|---|
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 |
227
+ | `equals(val)` | Strict equality (`===`) |
228
+ | `notEquals(val)` | Strict inequality (`!==`) |
229
+ | `isEmpty` | `undefined`, `null`, or `''` |
230
+ | `isNotEmpty` | Not `undefined`, `null`, or `''` |
231
+ | `isIn(arr)` | Value is in the given array |
232
+ | `isNotIn(arr)` | Value is not in the given array |
233
+
234
+ > `isEmpty` and `isNotEmpty` are constants. The rest are factory functions.
157
235
 
158
236
  ### Number
159
237
 
160
- | Decorator | Description |
238
+ | Rule | Description |
161
239
  |---|---|
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` |
240
+ | `min(n, opts?)` | `value >= n` (supports `{ exclusive: true }`) |
241
+ | `max(n, opts?)` | `value <= n` (supports `{ exclusive: true }`) |
242
+ | `isPositive` | `value > 0` |
243
+ | `isNegative` | `value < 0` |
244
+ | `isDivisibleBy(n)` | `value % n === 0` |
245
+
246
+ > `isPositive` and `isNegative` are constants (no parentheses). `min()`, `max()`, and `isDivisibleBy()` are factory functions.
168
247
 
169
248
  ### String
170
249
 
171
- <details>
172
- <summary>50+ string validators — click to expand</summary>
250
+ All string rules require the value to be a `string` type.
251
+
252
+ | Rule | Kind | Description |
253
+ |---|---|---|
254
+ | `minLength(n)` | factory | Minimum length |
255
+ | `maxLength(n)` | factory | Maximum length |
256
+ | `length(min, max)` | factory | Length range |
257
+ | `contains(seed)` | factory | Contains substring |
258
+ | `notContains(seed)` | factory | Does not contain substring |
259
+ | `matches(pattern, modifiers?)` | factory | Regex match |
260
+ | `isLowercase` | constant | All lowercase |
261
+ | `isUppercase` | constant | All uppercase |
262
+ | `isAscii` | constant | ASCII only |
263
+ | `isAlpha` | constant | Alphabetic only (en-US) |
264
+ | `isAlphanumeric` | constant | Alphanumeric only (en-US) |
265
+ | `isBooleanString` | constant | `'true'`, `'false'`, `'1'`, or `'0'` |
266
+ | `isNumberString(opts?)` | factory | Numeric string |
267
+ | `isDecimal(opts?)` | factory | Decimal string |
268
+ | `isFullWidth` | constant | Full-width characters |
269
+ | `isHalfWidth` | constant | Half-width characters |
270
+ | `isVariableWidth` | constant | Mix of full-width and half-width |
271
+ | `isMultibyte` | constant | Multibyte characters |
272
+ | `isSurrogatePair` | constant | Surrogate pair characters |
273
+ | `isHexadecimal` | constant | Hexadecimal string |
274
+ | `isOctal` | constant | Octal string |
275
+ | `isEmail(opts?)` | factory | Email format |
276
+ | `isURL(opts?)` | factory | URL format (port range validated) |
277
+ | `isUUID(version?)` | factory | UUID v1-v5 |
278
+ | `isIP(version?)` | factory | IPv4 / IPv6 |
279
+ | `isHexColor` | constant | Hex color (`#fff`, `#ffffff`) |
280
+ | `isRgbColor(includePercent?)` | factory | RGB color string |
281
+ | `isHSL` | constant | HSL color string |
282
+ | `isMACAddress(opts?)` | factory | MAC address |
283
+ | `isISBN(version?)` | factory | ISBN-10 / ISBN-13 |
284
+ | `isISIN` | constant | ISIN (International Securities Identification Number) |
285
+ | `isISO8601(opts?)` | factory | ISO 8601 date string |
286
+ | `isISRC` | constant | ISRC (International Standard Recording Code) |
287
+ | `isISSN(opts?)` | factory | ISSN (International Standard Serial Number) |
288
+ | `isJWT` | constant | JSON Web Token |
289
+ | `isLatLong(opts?)` | factory | Latitude/longitude string |
290
+ | `isLocale` | constant | Locale string (e.g. `en_US`) |
291
+ | `isDataURI` | constant | Data URI |
292
+ | `isFQDN(opts?)` | factory | Fully qualified domain name |
293
+ | `isPort` | constant | Port number string (0-65535) |
294
+ | `isEAN` | constant | EAN (European Article Number) |
295
+ | `isISO31661Alpha2` | constant | ISO 3166-1 alpha-2 country code |
296
+ | `isISO31661Alpha3` | constant | ISO 3166-1 alpha-3 country code |
297
+ | `isBIC` | constant | BIC (Bank Identification Code) / SWIFT code |
298
+ | `isFirebasePushId` | constant | Firebase Push ID |
299
+ | `isSemVer` | constant | Semantic version string |
300
+ | `isMongoId` | constant | MongoDB ObjectId (24-char hex) |
301
+ | `isJSON` | constant | Parseable JSON string |
302
+ | `isBase32(opts?)` | factory | Base32 encoded |
303
+ | `isBase58` | constant | Base58 encoded |
304
+ | `isBase64(opts?)` | factory | Base64 encoded |
305
+ | `isDateString(opts?)` | factory | Date string (configurable strict mode) |
306
+ | `isMimeType` | constant | MIME type string |
307
+ | `isCurrency(opts?)` | factory | Currency string |
308
+ | `isMagnetURI` | constant | Magnet URI |
309
+ | `isCreditCard` | constant | Credit card number (Luhn) |
310
+ | `isIBAN(opts?)` | factory | IBAN |
311
+ | `isByteLength(min, max?)` | factory | Byte length range |
312
+ | `isHash(algorithm)` | factory | Hash string (md4, md5, sha1, sha256, sha384, sha512, etc.) |
313
+ | `isRFC3339` | constant | RFC 3339 date-time string |
314
+ | `isMilitaryTime` | constant | Military time (HH:MM) |
315
+ | `isLatitude` | constant | Latitude string |
316
+ | `isLongitude` | constant | Longitude string |
317
+ | `isEthereumAddress` | constant | Ethereum address |
318
+ | `isBtcAddress` | constant | Bitcoin address |
319
+ | `isISO4217CurrencyCode` | constant | ISO 4217 currency code |
320
+ | `isPhoneNumber` | constant | E.164 international phone number |
321
+ | `isStrongPassword(opts?)` | factory | Strong password (configurable min length, uppercase, lowercase, numbers, symbols) |
322
+ | `isTaxId(locale)` | factory | Tax ID for given locale |
173
323
 
174
- | Decorator | Description |
324
+ ### Array
325
+
326
+ | Rule | Description |
175
327
  |---|---|
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>
328
+ | `arrayContains(values)` | Contains all given elements |
329
+ | `arrayNotContains(values)` | Contains none of the given elements |
330
+ | `arrayMinSize(n)` | Minimum array length |
331
+ | `arrayMaxSize(n)` | Maximum array length |
332
+ | `arrayUnique()` | No duplicates |
333
+ | `arrayNotEmpty()` | Not empty |
223
334
 
224
335
  ### Date
225
336
 
226
- | Decorator | Description |
337
+ | Rule | Description |
227
338
  |---|---|
228
- | `@MinDate(date)` | Minimum date |
229
- | `@MaxDate(date)` | Maximum date |
339
+ | `minDate(date)` | Minimum date |
340
+ | `maxDate(date)` | Maximum date |
230
341
 
231
- ### Array
342
+ ### Object
232
343
 
233
- | Decorator | Description |
344
+ | Rule | Description |
234
345
  |---|---|
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 |
346
+ | `isNotEmptyObject(opts?)` | At least one key (supports `{ nullable: true }` to ignore null-valued keys) |
347
+ | `isInstance(Class)` | `instanceof` check against given class |
241
348
 
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 |
349
+ ### Locale
250
350
 
251
- ### Transform & Type
351
+ Locale-specific validators that accept a locale string parameter.
252
352
 
253
- | Decorator | Description |
353
+ | Rule | Description |
254
354
  |---|---|
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) |
355
+ | `isMobilePhone(locale)` | Mobile phone number for the given locale (e.g. `'ko-KR'`, `'en-US'`, `'ja-JP'`) |
356
+ | `isPostalCode(locale)` | Postal code for the given locale/country code (e.g. `'US'`, `'KR'`, `'GB'`) |
357
+ | `isIdentityCard(locale)` | National identity card number for the given locale (e.g. `'KR'`, `'US'`, `'CN'`) |
358
+ | `isPassportNumber(locale)` | Passport number for the given locale (e.g. `'US'`, `'KR'`, `'GB'`) |
261
359
 
262
360
  ---
263
361
 
264
- ## ⚙️ Validation Options
362
+ ## Configuration
265
363
 
266
- Every validation decorator accepts `ValidationOptions` as its last argument:
364
+ Call `configure()` **before** the first `deserialize()`/`serialize()`:
267
365
 
268
366
  ```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
- }
367
+ import { configure } from '@zipbul/baker';
368
+
369
+ configure({
370
+ autoConvert: false, // Implicit type conversion ("123" -> 123)
371
+ allowClassDefaults: false, // Use class default values for missing keys
372
+ stopAtFirstError: false, // Stop at first error or collect all
373
+ forbidUnknown: false, // Reject undeclared fields
374
+ debug: false, // Emit field exclusion comments in generated code
375
+ });
279
376
  ```
280
377
 
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
- ```
378
+ `configure()` returns `{ warnings: string[] }` — if called after auto-seal, warnings describe which classes won't be affected.
295
379
 
296
380
  ---
297
381
 
298
- ## 🚨 Error Handling
382
+ ## Error Handling
299
383
 
300
384
  When validation fails, `deserialize()` throws a `BakerValidationError`:
301
385
 
@@ -304,86 +388,50 @@ class BakerValidationError extends Error {
304
388
  readonly errors: BakerError[];
305
389
  readonly className: string;
306
390
  }
307
- ```
308
391
 
309
- Each error follows the `BakerError` interface:
310
-
311
- ```typescript
312
392
  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[];
393
+ readonly path: string; // 'user.address.city'
394
+ readonly code: string; // 'isString', 'min', 'isEmail'
395
+ readonly message?: string; // Custom message
396
+ readonly context?: unknown; // Custom context
330
397
  }
331
398
  ```
332
399
 
333
400
  ---
334
401
 
335
- ## 🏷️ Group-based Validation
402
+ ## Nested Objects
336
403
 
337
- Apply different rules depending on the use case:
404
+ Use `type` option for nested DTO validation:
338
405
 
339
406
  ```typescript
340
- class UserDto {
341
- @IsString({ groups: ['create'] })
342
- name!: string;
343
-
344
- @IsEmail({ groups: ['create', 'update'] })
345
- email!: string;
346
- }
347
-
348
- // Only validate rules in the 'create' group
349
- const user = await deserialize(UserDto, body, { groups: ['create'] });
350
- ```
351
-
352
- ---
353
-
354
- ## 🪆 Nested Objects
355
-
356
- Use `@Nested` for nested DTO validation:
357
-
358
- ```typescript
359
- import { Nested, IsString } from '@zipbul/baker';
360
-
361
407
  class AddressDto {
362
- @IsString()
408
+ @Field(isString)
363
409
  city!: string;
364
410
  }
365
411
 
366
412
  class UserDto {
367
- @Nested(() => AddressDto)
413
+ @Field({ type: () => AddressDto })
368
414
  address!: AddressDto;
415
+
416
+ // Array of nested DTOs
417
+ @Field({ type: () => [AddressDto] })
418
+ addresses!: AddressDto[];
369
419
  }
370
420
  ```
371
421
 
372
- Array nesting and discriminator polymorphism:
422
+ ### Discriminator (Polymorphism)
373
423
 
374
424
  ```typescript
375
- class ItemDto {
376
- @IsString()
377
- label!: string;
425
+ class DogDto {
426
+ @Field(isString) breed!: string;
378
427
  }
379
-
380
- class ListDto {
381
- @Nested(() => ItemDto, { each: true })
382
- items!: ItemDto[];
428
+ class CatDto {
429
+ @Field(isBoolean) indoor!: boolean;
383
430
  }
384
431
 
385
432
  class PetOwnerDto {
386
- @Nested(() => DogDto, {
433
+ @Field({
434
+ type: () => DogDto,
387
435
  discriminator: {
388
436
  property: 'type',
389
437
  subTypes: [
@@ -396,105 +444,145 @@ class PetOwnerDto {
396
444
  }
397
445
  ```
398
446
 
447
+ Discriminator works in both directions — `deserialize()` switches on the property value, `serialize()` dispatches via `instanceof`.
448
+
399
449
  ---
400
450
 
401
- ## 🧩 Custom Rules
451
+ ## Inheritance
402
452
 
403
- Create user-defined validation rules with codegen support:
453
+ Baker supports class inheritance. Child DTOs automatically inherit all `@Field()` decorators from parent classes. You can override or extend fields in child classes:
404
454
 
405
455
  ```typescript
406
- import { createRule } from '@zipbul/baker';
456
+ class BaseDto {
457
+ @Field(isString)
458
+ name!: string;
459
+ }
407
460
 
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
- });
461
+ class ExtendedDto extends BaseDto {
462
+ @Field(isInt, min(0))
463
+ age!: number;
464
+ // `name` is inherited from BaseDto
465
+ }
414
466
  ```
415
467
 
416
468
  ---
417
469
 
418
- ## ⚙️ Seal Options
470
+ ## Transform
471
+
472
+ The `transform` option in `FieldOptions` lets you transform values during deserialization and/or serialization. Transform functions can be **async**.
419
473
 
420
474
  ```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
- });
475
+ class UserDto {
476
+ @Field(isString, {
477
+ transform: ({ value, direction }) => {
478
+ return direction === 'deserialize'
479
+ ? (value as string).trim().toLowerCase()
480
+ : value;
481
+ },
482
+ })
483
+ email!: string;
484
+
485
+ @Field(isString, {
486
+ transform: async ({ value }) => {
487
+ return await someAsyncOperation(value);
488
+ },
489
+ transformDirection: 'deserializeOnly',
490
+ })
491
+ data!: string;
492
+ }
429
493
  ```
430
494
 
431
495
  ---
432
496
 
433
- ## 📐 JSON Schema
434
-
435
- Generate JSON Schema Draft 2020-12 from your DTOs:
497
+ ## Custom Rules
436
498
 
437
499
  ```typescript
438
- import { toJsonSchema } from '@zipbul/baker';
500
+ import { createRule } from '@zipbul/baker';
439
501
 
440
- const schema = toJsonSchema(CreateUserDto);
441
- // { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { ... } }
502
+ const isPositiveInt = createRule({
503
+ name: 'isPositiveInt',
504
+ validate: (value) => Number.isInteger(value) && (value as number) > 0,
505
+ });
506
+
507
+ class Dto {
508
+ @Field(isPositiveInt)
509
+ count!: number;
510
+ }
442
511
  ```
443
512
 
444
- Options:
513
+ ---
514
+
515
+ ## Class-level JSON Schema Metadata
516
+
517
+ Use `collectClassSchema()` to attach class-level JSON Schema metadata (title, description, etc.) to a DTO. This metadata is merged into the output of `toJsonSchema()`.
518
+
519
+ > `collectClassSchema` is a low-level API exported from `src/collect.ts`. It is not available as a subpath export and must be imported directly.
445
520
 
446
521
  ```typescript
447
- toJsonSchema(CreateUserDto, {
448
- direction: 'deserialize', // 'deserialize' | 'serialize' — filters @Expose/@Exclude direction
449
- groups: ['create'], // Filter rules/fields by group
522
+ import { collectClassSchema } from '@zipbul/baker/src/collect';
523
+
524
+ class CreateUserDto {
525
+ @Field(isString) name!: string;
526
+ @Field(isEmail()) email!: string;
527
+ }
528
+
529
+ collectClassSchema(CreateUserDto, {
530
+ title: 'CreateUserRequest',
531
+ description: 'Payload for creating a new user',
450
532
  });
451
533
  ```
452
534
 
453
- Use `@Schema()` to attach additional JSON Schema metadata:
535
+ For property-level schema overrides, use the `schema` option in `@Field()`:
454
536
 
455
537
  ```typescript
456
- @Schema({ title: 'CreateUser', description: 'Creates a new user' })
457
- class CreateUserDto {
458
- @IsString()
459
- @Schema({ description: 'Display name', examples: ['Alice'] })
538
+ class Dto {
539
+ @Field(isString, minLength(1), {
540
+ schema: { description: 'User display name', minLength: 5 },
541
+ })
460
542
  name!: string;
461
543
  }
462
544
  ```
463
545
 
464
546
  ---
465
547
 
466
- ## 📂 Subpath Exports
548
+ ## JSON Schema
467
549
 
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 |
550
+ Generate JSON Schema Draft 2020-12 from your DTOs:
551
+
552
+ ```typescript
553
+ import { toJsonSchema } from '@zipbul/baker';
554
+
555
+ const schema = toJsonSchema(CreateUserDto, {
556
+ direction: 'deserialize', // 'deserialize' | 'serialize'
557
+ groups: ['create'], // Filter by group
558
+ onUnmappedRule: (name) => { /* custom rules without schema mapping */ },
559
+ });
560
+ ```
474
561
 
475
562
  ---
476
563
 
477
- ## 🔍 How It Works
564
+ ## How It Works
478
565
 
479
566
  ```
480
- ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
481
- │ Decorators │ ──▶ │ seal() │ ──▶ │ new Function() code
482
- │ (metadata) │ │ at startup │ │ (inline codegen) │
483
- └─────────────┘ └──────────────┘ └──────────┬──────────┘
484
-
485
- ┌──────────▼──────────┐
486
- │ deserialize() / │
487
- │ serialize() │
488
- │ (execute generated) │
489
- └─────────────────────┘
567
+ Decorators (@Field) auto-seal (first call) deserialize() / serialize()
568
+ metadata -> new Function() codegen -> execute generated code
490
569
  ```
491
570
 
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
571
+ 1. `@Field()` attaches validation metadata to class properties at definition time
572
+ 2. First `deserialize()`/`serialize()` call triggers **auto-seal** — reads all metadata, analyzes circular references, generates optimized JavaScript functions via `new Function()`
573
+ 3. Subsequent calls execute the generated function directly — no interpretation loops
574
+
575
+ ---
576
+
577
+ ## Subpath Exports
578
+
579
+ | Import path | Purpose |
580
+ |---|---|
581
+ | `@zipbul/baker` | Main API: `deserialize`, `serialize`, `configure`, `toJsonSchema`, `Field`, `arrayOf`, `createRule` |
582
+ | `@zipbul/baker/rules` | Rule functions and constants: `isString`, `min()`, `isEmail()`, `arrayOf()`, etc. |
495
583
 
496
584
  ---
497
585
 
498
- ## 📄 License
586
+ ## License
499
587
 
500
- [MIT](./LICENSE) © [Junhyung Park](https://github.com/parkrevil)
588
+ [MIT](./LICENSE)