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