@zipbul/baker 1.0.0 → 2.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.
package/README.md CHANGED
@@ -1,588 +1,267 @@
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
- ---
22
-
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 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
52
-
53
- ---
54
-
55
- ## Installation
1
+ # @zipbul/baker
2
+
3
+ Decorator-based validation + transformation with inline code generation. Zero `reflect-metadata`.
56
4
 
57
5
  ```bash
58
6
  bun add @zipbul/baker
59
7
  ```
60
8
 
61
- > **Requirements:** Bun >= 1.0, `experimentalDecorators: true` in tsconfig.json
62
-
63
- ```jsonc
64
- // tsconfig.json
65
- {
66
- "compilerOptions": {
67
- "experimentalDecorators": true
68
- }
69
- }
70
- ```
9
+ Requires `"experimentalDecorators": true` in `tsconfig.json`.
71
10
 
72
- ---
11
+ ## API
73
12
 
74
- ## Quick Start
13
+ ### `deserialize<T>(Class, input, options?): T | BakerErrors | Promise<T | BakerErrors>`
75
14
 
76
- ### 1. Define a DTO
15
+ Validates input and creates a class instance. Sync DTOs return directly. Async DTOs (async transform/rules) return Promise. Always safe to `await`.
77
16
 
78
17
  ```typescript
79
- import { Field } from '@zipbul/baker';
80
- import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
18
+ import { deserialize, isBakerError, Field } from '@zipbul/baker';
19
+ import { isString, isNumber, isEmail, min, minLength } from '@zipbul/baker/rules';
81
20
 
82
- class CreateUserDto {
83
- @Field(isString)
84
- name!: string;
21
+ class UserDto {
22
+ @Field(isString, minLength(2)) name!: string;
23
+ @Field(isNumber(), min(0)) age!: number;
24
+ @Field(isString, isEmail()) email!: string;
25
+ }
85
26
 
86
- @Field(isInt, min(0), max(120))
87
- age!: number;
27
+ const result = await deserialize(UserDto, { name: 'Alice', age: 30, email: 'alice@test.com' });
88
28
 
89
- @Field(isEmail())
90
- email!: string;
29
+ if (isBakerError(result)) {
30
+ console.log(result.errors); // { path: string, code: string }[]
31
+ } else {
32
+ console.log(result.name); // 'Alice'
91
33
  }
92
34
  ```
93
35
 
94
- ### 2. Deserialize (auto-seals on first call)
36
+ Never throws on validation failure. Throws `SealError` only for programming errors (no `@Field` decorators, banned field names).
95
37
 
96
- ```typescript
97
- import { deserialize, BakerValidationError } from '@zipbul/baker';
98
-
99
- try {
100
- const user = await deserialize(CreateUserDto, requestBody);
101
- // user is a validated CreateUserDto instance
102
- } catch (e) {
103
- if (e instanceof BakerValidationError) {
104
- console.log(e.errors); // BakerError[]
105
- }
106
- }
107
- ```
38
+ ### `validate(Class, input, options?): true | BakerErrors | Promise<true | BakerErrors>`
108
39
 
109
- ### 3. Serialize
40
+ Same validation as `deserialize` without instance creation.
110
41
 
111
42
  ```typescript
112
- import { serialize } from '@zipbul/baker';
43
+ import { validate, isBakerError } from '@zipbul/baker';
113
44
 
114
- const plain = await serialize(userInstance);
115
- // plain: Record<string, unknown>
45
+ const result = await validate(UserDto, input);
46
+ if (isBakerError(result)) { /* errors */ }
116
47
  ```
117
48
 
118
- > No `seal()` call needed baker auto-seals all registered DTOs on the first `deserialize()` or `serialize()` call.
49
+ ### `validate(input, ...rules): true | BakerErrors | Promise<true | BakerErrors>`
119
50
 
120
- ---
51
+ Ad-hoc single value validation. No DTO needed.
121
52
 
122
- ## The `@Field()` Decorator
53
+ ```typescript
54
+ const result = await validate('hello@test.com', isString, isEmail());
55
+ // result === true
56
+ ```
123
57
 
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.
58
+ ### `serialize<T>(instance, options?): Record<string, unknown> | Promise<Record<string, unknown>>`
125
59
 
126
- ### Signatures
60
+ Converts a class instance to a plain object. No validation. Sync DTOs return directly.
127
61
 
128
62
  ```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'] })
63
+ import { serialize } from '@zipbul/baker';
137
64
 
138
- // No rules (plain field)
139
- @Field()
65
+ const plain = await serialize(userInstance);
140
66
  ```
141
67
 
142
- ### FieldOptions
68
+ ### `isBakerError(value): value is BakerErrors`
69
+
70
+ Type guard. Narrows `deserialize`/`validate` result to error type.
143
71
 
144
72
  ```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';
73
+ interface BakerError {
74
+ readonly path: string; // 'name', 'address.city', 'items[0].value'
75
+ readonly code: string; // 'isString', 'minLength', 'invalidInput'
76
+ readonly message?: string; // custom message if set
77
+ readonly context?: unknown; // custom context if set
164
78
  }
165
79
  ```
166
80
 
167
- ### Per-rule Options (message, groups)
81
+ ### `configure(config): ConfigureResult`
168
82
 
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:
83
+ Global configuration. Call before first `deserialize`/`serialize`/`validate`.
170
84
 
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)
85
+ ```typescript
86
+ import { configure } from '@zipbul/baker';
174
87
 
175
- ### `arrayOf()` — Array Element Validation
88
+ configure({
89
+ autoConvert: true, // "123" → 123. Default: false
90
+ allowClassDefaults: true, // use class field initializers for missing keys. Default: false
91
+ stopAtFirstError: true, // return on first validation failure. Default: false
92
+ forbidUnknown: true, // reject undeclared fields. Default: false
93
+ });
94
+ ```
176
95
 
177
- `arrayOf()` applies rules to each element of an array. Import it from `@zipbul/baker/rules` or `@zipbul/baker`.
96
+ ### `createRule(name, validate): EmittableRule`
97
+
98
+ Creates a custom validation rule.
178
99
 
179
100
  ```typescript
180
- import { Field, arrayOf } from '@zipbul/baker';
181
- import { isString, minLength } from '@zipbul/baker/rules';
101
+ import { createRule } from '@zipbul/baker';
182
102
 
183
- class TagsDto {
184
- @Field(arrayOf(isString, minLength(1)))
185
- tags!: string[];
186
- }
103
+ const isEven = createRule('isEven', (v) => typeof v === 'number' && v % 2 === 0);
104
+
105
+ const isUnique = createRule({
106
+ name: 'isUnique',
107
+ validate: async (v) => await db.checkUnique(v),
108
+ constraints: { table: 'users' },
109
+ });
187
110
  ```
188
111
 
189
- You can mix `arrayOf()` with top-level array rules:
112
+ ## `@Field` Decorator
190
113
 
191
114
  ```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
- }
115
+ @Field(...rules)
116
+ @Field(...rules, options)
117
+ @Field(options)
198
118
  ```
199
119
 
200
- ---
201
-
202
- ## Built-in Rules
120
+ ### Options
203
121
 
204
- All rules are imported from `@zipbul/baker/rules` and passed as arguments to `@Field()`.
122
+ ```typescript
123
+ interface FieldOptions {
124
+ type?: () => DtoClass | [DtoClass]; // nested DTO. [Dto] for arrays
125
+ discriminator?: { // polymorphic dispatch
126
+ property: string;
127
+ subTypes: { value: Function; name: string }[];
128
+ };
129
+ keepDiscriminatorProperty?: boolean; // preserve discriminator in result. Default: false
130
+ rules?: EmittableRule[]; // rules as array (alternative to variadic)
131
+ optional?: boolean; // allow undefined. Default: false
132
+ nullable?: boolean; // allow null. Default: false
133
+ name?: string; // bidirectional key mapping
134
+ deserializeName?: string; // input key mapping
135
+ serializeName?: string; // output key mapping
136
+ exclude?: boolean | 'deserializeOnly' | 'serializeOnly'; // field exclusion
137
+ groups?: string[]; // conditional visibility
138
+ when?: (obj: any) => boolean; // conditional validation
139
+ transform?: (params: FieldTransformParams) => unknown; // value transform
140
+ transformDirection?: 'deserializeOnly' | 'serializeOnly'; // transform direction
141
+ message?: string | ((args) => string); // error message override
142
+ context?: unknown; // error context
143
+ mapValue?: () => DtoClass; // Map value DTO
144
+ setValue?: () => DtoClass; // Set element DTO
145
+ }
146
+ ```
205
147
 
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.
148
+ ## Rules
207
149
 
208
150
  ### Type Checkers
209
151
 
210
- | Rule | Description |
211
- |---|---|
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.
222
-
223
- ### Common
224
-
225
- | Rule | Description |
226
- |---|---|
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.
235
-
236
- ### Number
237
-
238
- | Rule | Description |
239
- |---|---|
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.
247
-
248
- ### String
249
-
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 |
323
-
324
- ### Array
325
-
326
- | Rule | Description |
327
- |---|---|
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 |
152
+ `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject` — constants, no `()`.
334
153
 
335
- ### Date
154
+ `isNumber(options?)`, `isEnum(entity)` — factories, need `()`.
336
155
 
337
- | Rule | Description |
338
- |---|---|
339
- | `minDate(date)` | Minimum date |
340
- | `maxDate(date)` | Maximum date |
156
+ ### Numbers
341
157
 
342
- ### Object
158
+ `min(n)`, `max(n)`, `min(n, { exclusive: true })`, `isPositive`, `isNegative`, `isDivisibleBy(n)`
343
159
 
344
- | Rule | Description |
345
- |---|---|
346
- | `isNotEmptyObject(opts?)` | At least one key (supports `{ nullable: true }` to ignore null-valued keys) |
347
- | `isInstance(Class)` | `instanceof` check against given class |
160
+ ### Strings
348
161
 
349
- ### Locale
162
+ `minLength(n)`, `maxLength(n)`, `length(min, max)`, `contains(seed)`, `notContains(seed)`, `matches(regex)`
350
163
 
351
- Locale-specific validators that accept a locale string parameter.
164
+ ### Formats
352
165
 
353
- | Rule | Description |
354
- |---|---|
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'`) |
166
+ `isEmail()`, `isURL()`, `isUUID(version?)`, `isIP(version?)`, `isISO8601()`, `isJSON`, `isJWT`, `isCreditCard`, `isIBAN()`, `isFQDN()`, `isMACAddress()`, `isBase64()`, `isHexColor`, `isSemVer`, `isMongoId`, `isPhoneNumber`, `isStrongPassword()`
359
167
 
360
- ---
168
+ ### Arrays
361
169
 
362
- ## Configuration
170
+ `arrayMinSize(n)`, `arrayMaxSize(n)`, `arrayUnique()`, `arrayNotEmpty`, `arrayContains(values)`, `arrayOf(...rules)`
363
171
 
364
- Call `configure()` **before** the first `deserialize()`/`serialize()`:
172
+ ### Common
365
173
 
366
- ```typescript
367
- import { configure } from '@zipbul/baker';
174
+ `equals(val)`, `notEquals(val)`, `isIn(values)`, `isNotIn(values)`, `isEmpty`, `isNotEmpty`
368
175
 
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
- });
376
- ```
176
+ ### Date
377
177
 
378
- `configure()` returns `{ warnings: string[] }` — if called after auto-seal, warnings describe which classes won't be affected.
178
+ `minDate(date)`, `maxDate(date)`
379
179
 
380
- ---
180
+ ### Locale
381
181
 
382
- ## Error Handling
182
+ `isMobilePhone(locale)`, `isPostalCode(locale)`, `isIdentityCard(locale)`, `isPassportNumber(locale)`
383
183
 
384
- When validation fails, `deserialize()` throws a `BakerValidationError`:
184
+ ## Nested DTOs
385
185
 
386
186
  ```typescript
387
- class BakerValidationError extends Error {
388
- readonly errors: BakerError[];
389
- readonly className: string;
187
+ class AddressDto {
188
+ @Field(isString) city!: string;
390
189
  }
391
190
 
392
- interface BakerError {
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
191
+ class UserDto {
192
+ @Field({ type: () => AddressDto }) address!: AddressDto;
193
+ @Field({ type: () => [AddressDto] }) addresses!: AddressDto[];
397
194
  }
398
195
  ```
399
196
 
400
- ---
401
-
402
- ## Nested Objects
403
-
404
- Use `type` option for nested DTO validation:
197
+ ## Collections
405
198
 
406
199
  ```typescript
407
- class AddressDto {
408
- @Field(isString)
409
- city!: string;
410
- }
411
-
412
200
  class UserDto {
413
- @Field({ type: () => AddressDto })
414
- address!: AddressDto;
415
-
416
- // Array of nested DTOs
417
- @Field({ type: () => [AddressDto] })
418
- addresses!: AddressDto[];
201
+ @Field({ type: () => Set as any, setValue: () => TagDto }) tags!: Set<TagDto>;
202
+ @Field({ type: () => Map as any, mapValue: () => TagDto }) tagMap!: Map<string, TagDto>;
419
203
  }
420
204
  ```
421
205
 
422
- ### Discriminator (Polymorphism)
206
+ ## Discriminator
423
207
 
424
208
  ```typescript
425
- class DogDto {
426
- @Field(isString) breed!: string;
427
- }
428
- class CatDto {
429
- @Field(isBoolean) indoor!: boolean;
430
- }
431
-
432
- class PetOwnerDto {
209
+ class PetOwner {
433
210
  @Field({
434
- type: () => DogDto,
211
+ type: () => CatDto,
435
212
  discriminator: {
436
- property: 'type',
213
+ property: 'kind',
437
214
  subTypes: [
438
- { value: DogDto, name: 'dog' },
439
215
  { value: CatDto, name: 'cat' },
216
+ { value: DogDto, name: 'dog' },
440
217
  ],
441
218
  },
442
- })
443
- pet!: DogDto | CatDto;
219
+ }) pet!: CatDto | DogDto;
444
220
  }
445
221
  ```
446
222
 
447
- Discriminator works in both directions — `deserialize()` switches on the property value, `serialize()` dispatches via `instanceof`.
448
-
449
- ---
450
-
451
223
  ## Inheritance
452
224
 
453
- Baker supports class inheritance. Child DTOs automatically inherit all `@Field()` decorators from parent classes. You can override or extend fields in child classes:
454
-
455
225
  ```typescript
456
226
  class BaseDto {
457
- @Field(isString)
458
- name!: string;
459
- }
460
-
461
- class ExtendedDto extends BaseDto {
462
- @Field(isInt, min(0))
463
- age!: number;
464
- // `name` is inherited from BaseDto
465
- }
466
- ```
467
-
468
- ---
469
-
470
- ## Transform
471
-
472
- The `transform` option in `FieldOptions` lets you transform values during deserialization and/or serialization. Transform functions can be **async**.
473
-
474
- ```typescript
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
- }
493
- ```
494
-
495
- ---
496
-
497
- ## Custom Rules
498
-
499
- ```typescript
500
- import { createRule } from '@zipbul/baker';
501
-
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;
227
+ @Field(isString) id!: string;
510
228
  }
511
- ```
512
229
 
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.
520
-
521
- ```typescript
522
- import { collectClassSchema } from '@zipbul/baker/src/collect';
523
-
524
- class CreateUserDto {
230
+ class UserDto extends BaseDto {
525
231
  @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',
532
- });
533
- ```
534
-
535
- For property-level schema overrides, use the `schema` option in `@Field()`:
536
-
537
- ```typescript
538
- class Dto {
539
- @Field(isString, minLength(1), {
540
- schema: { description: 'User display name', minLength: 5 },
541
- })
542
- name!: string;
232
+ // inherits 'id' field with isString rule
543
233
  }
544
234
  ```
545
235
 
546
- ---
547
-
548
- ## JSON Schema
549
-
550
- Generate JSON Schema Draft 2020-12 from your DTOs:
236
+ ## Exports
551
237
 
552
238
  ```typescript
553
- import { toJsonSchema } from '@zipbul/baker';
239
+ // Functions
240
+ import { deserialize, validate, serialize, configure, createRule } from '@zipbul/baker';
554
241
 
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
- ```
242
+ // Decorators
243
+ import { Field, arrayOf } from '@zipbul/baker';
561
244
 
562
- ---
245
+ // Error handling
246
+ import { isBakerError, SealError } from '@zipbul/baker';
563
247
 
564
- ## How It Works
248
+ // Types
249
+ import type {
250
+ BakerError, BakerErrors, FieldOptions, FieldTransformParams,
251
+ ArrayOfMarker, EmittableRule, BakerConfig, ConfigureResult, RuntimeOptions,
252
+ } from '@zipbul/baker';
565
253
 
254
+ // Rules (subpath)
255
+ import { isString, isNumber, ... } from '@zipbul/baker/rules';
566
256
  ```
567
- Decorators (@Field) auto-seal (first call) deserialize() / serialize()
568
- metadata -> new Function() codegen -> execute generated code
569
- ```
570
-
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
257
 
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. |
258
+ ## What Baker Does Not Do
583
259
 
584
- ---
260
+ - JSON Schema / OpenAPI generation
261
+ - GraphQL schema generation
262
+ - Runtime type inference from schemas
263
+ - `reflect-metadata` dependency
585
264
 
586
265
  ## License
587
266
 
588
- [MIT](./LICENSE)
267
+ MIT