@cleverbrush/schema 2.0.0 → 3.0.1

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 (87) hide show
  1. package/README.md +412 -8
  2. package/dist/builders/AnySchemaBuilder.js +1 -1
  3. package/dist/builders/ArraySchemaBuilder.d.ts +2 -0
  4. package/dist/builders/ArraySchemaBuilder.js +1 -1
  5. package/dist/builders/BooleanSchemaBuilder.d.ts +15 -0
  6. package/dist/builders/BooleanSchemaBuilder.js +1 -1
  7. package/dist/builders/DateSchemaBuilder.d.ts +20 -0
  8. package/dist/builders/DateSchemaBuilder.js +1 -1
  9. package/dist/builders/ExternSchemaBuilder.d.ts +2 -0
  10. package/dist/builders/ExternSchemaBuilder.js +1 -1
  11. package/dist/builders/FunctionSchemaBuilder.d.ts +162 -18
  12. package/dist/builders/FunctionSchemaBuilder.js +1 -1
  13. package/dist/builders/GenericSchemaBuilder.d.ts +294 -0
  14. package/dist/builders/LazySchemaBuilder.d.ts +2 -0
  15. package/dist/builders/NumberSchemaBuilder.d.ts +16 -0
  16. package/dist/builders/NumberSchemaBuilder.js +1 -1
  17. package/dist/builders/ObjectSchemaBuilder.d.ts +262 -42
  18. package/dist/builders/ObjectSchemaBuilder.js +1 -1
  19. package/dist/builders/ParseStringSchemaBuilder.d.ts +204 -0
  20. package/dist/builders/ParseStringSchemaBuilder.js +2 -0
  21. package/dist/builders/ParseStringSchemaBuilder.js.map +1 -0
  22. package/dist/builders/PromiseSchemaBuilder.d.ts +213 -0
  23. package/dist/builders/PromiseSchemaBuilder.js +2 -0
  24. package/dist/builders/PromiseSchemaBuilder.js.map +1 -0
  25. package/dist/builders/RecordSchemaBuilder.d.ts +2 -0
  26. package/dist/builders/RecordSchemaBuilder.js +1 -1
  27. package/dist/builders/SchemaBuilder.d.ts +89 -0
  28. package/dist/builders/StringSchemaBuilder.d.ts +2 -0
  29. package/dist/builders/StringSchemaBuilder.js +1 -1
  30. package/dist/builders/TupleSchemaBuilder.d.ts +2 -0
  31. package/dist/builders/TupleSchemaBuilder.js +1 -1
  32. package/dist/builders/UnionSchemaBuilder.d.ts +8 -0
  33. package/dist/builders/UnionSchemaBuilder.js +1 -1
  34. package/dist/chunk-3JMDGYDT.js +2 -0
  35. package/dist/chunk-3JMDGYDT.js.map +1 -0
  36. package/dist/{chunk-KESLT5EE.js → chunk-BUEVZ3KA.js} +2 -2
  37. package/dist/{chunk-KESLT5EE.js.map → chunk-BUEVZ3KA.js.map} +1 -1
  38. package/dist/{chunk-KFTO7TMB.js → chunk-CFIJQ4GP.js} +2 -2
  39. package/dist/{chunk-KFTO7TMB.js.map → chunk-CFIJQ4GP.js.map} +1 -1
  40. package/dist/chunk-DY7J6RNN.js +2 -0
  41. package/dist/chunk-DY7J6RNN.js.map +1 -0
  42. package/dist/{chunk-YWBNVHR6.js → chunk-EIVZX4ZO.js} +2 -2
  43. package/dist/{chunk-YWBNVHR6.js.map → chunk-EIVZX4ZO.js.map} +1 -1
  44. package/dist/chunk-GXPV6UQK.js +2 -0
  45. package/dist/chunk-GXPV6UQK.js.map +1 -0
  46. package/dist/{chunk-ZAN4ZHCJ.js → chunk-HN774HD7.js} +2 -2
  47. package/dist/chunk-K6Z47OQY.js +2 -0
  48. package/dist/chunk-K6Z47OQY.js.map +1 -0
  49. package/dist/chunk-NUW3VXZV.js +2 -0
  50. package/dist/chunk-NUW3VXZV.js.map +1 -0
  51. package/dist/{chunk-MKAYFPAR.js → chunk-PHE4LIAN.js} +2 -2
  52. package/dist/chunk-QARCEYGO.js +2 -0
  53. package/dist/chunk-QARCEYGO.js.map +1 -0
  54. package/dist/chunk-WDMJBGBD.js +2 -0
  55. package/dist/chunk-WDMJBGBD.js.map +1 -0
  56. package/dist/chunk-WQDYWDOE.js +2 -0
  57. package/dist/chunk-WQDYWDOE.js.map +1 -0
  58. package/dist/{chunk-FWUEUW2N.js → chunk-YQZHDMRF.js} +2 -2
  59. package/dist/chunk-ZC6YBKCP.js +2 -0
  60. package/dist/chunk-ZC6YBKCP.js.map +1 -0
  61. package/dist/{chunk-IJ6FO37G.js → chunk-ZFI27R3L.js} +2 -2
  62. package/dist/core.d.ts +7 -2
  63. package/dist/core.js +1 -1
  64. package/dist/extension.d.ts +10 -2
  65. package/dist/extensions/index.d.ts +5 -1
  66. package/dist/index.d.ts +4 -1
  67. package/dist/index.js +1 -1
  68. package/dist/index.js.map +1 -1
  69. package/package.json +12 -4
  70. package/dist/chunk-BFCMDDTX.js +0 -2
  71. package/dist/chunk-BFCMDDTX.js.map +0 -1
  72. package/dist/chunk-CTP4RHDG.js +0 -2
  73. package/dist/chunk-CTP4RHDG.js.map +0 -1
  74. package/dist/chunk-EUQ5AE3H.js +0 -2
  75. package/dist/chunk-EUQ5AE3H.js.map +0 -1
  76. package/dist/chunk-NXPH3ZUW.js +0 -2
  77. package/dist/chunk-NXPH3ZUW.js.map +0 -1
  78. package/dist/chunk-S5TC6NSU.js +0 -2
  79. package/dist/chunk-S5TC6NSU.js.map +0 -1
  80. package/dist/chunk-Z7QZ7IXT.js +0 -2
  81. package/dist/chunk-Z7QZ7IXT.js.map +0 -1
  82. package/dist/chunk-ZUPYV5TI.js +0 -2
  83. package/dist/chunk-ZUPYV5TI.js.map +0 -1
  84. /package/dist/{chunk-ZAN4ZHCJ.js.map → chunk-HN774HD7.js.map} +0 -0
  85. /package/dist/{chunk-MKAYFPAR.js.map → chunk-PHE4LIAN.js.map} +0 -0
  86. /package/dist/{chunk-FWUEUW2N.js.map → chunk-YQZHDMRF.js.map} +0 -0
  87. /package/dist/{chunk-IJ6FO37G.js.map → chunk-ZFI27R3L.js.map} +0 -0
package/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  [![CI](https://github.com/cleverbrush/framework/actions/workflows/ci.yml/badge.svg)](https://github.com/cleverbrush/framework/actions/workflows/ci.yml)
4
4
  [![Standard Schema v1](https://img.shields.io/badge/Standard%20Schema-v1-blue)](https://standardschema.dev/)
5
5
  <!-- bundle-badge-start -->
6
- [![Bundle size](https://img.shields.io/badge/bundle-17.2%20KB%20gzip-green)](https://github.com/cleverbrush/framework/blob/master/libs/schema)
6
+ [![Bundle size](https://img.shields.io/badge/bundle-19.9%20KB%20gzip-green)](https://github.com/cleverbrush/framework/blob/master/libs/schema)
7
7
  <!-- bundle-badge-end -->
8
8
  [![License: BSD-3-Clause](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](../../LICENSE)
9
9
  <!-- coverage-badge-start -->
10
- ![Coverage](https://img.shields.io/badge/coverage-98.7%25-brightgreen)
10
+ ![Coverage](https://img.shields.io/badge/coverage-96.8%25-brightgreen)
11
11
  <!-- coverage-badge-end -->
12
12
 
13
13
  A schema definition and validation library for TypeScript — faster than Zod in 14/15 benchmarks (up to 204× faster on invalid input), 3× smaller than Zod v4, and compatible with 50+ ecosystem tools via [Standard Schema v1](https://standardschema.dev/).
@@ -126,10 +126,11 @@ The following builder functions are available:
126
126
  | --------------- | ------------------------------------------------- | --------------------------------------------------------------- |
127
127
  | `any()` | Any value. Similar to TypeScript's `any` type. | `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)`, `.addValidator(fn)` |
128
128
  | `string()` | String value with constraints. | `.minLength(n)`, `.maxLength(n)`, `.matches(re)`, `.email()`, `.url()`, `.uuid()`, `.ip()`, `.trim()`, `.toLowerCase()`, `.nonempty()`, `.oneOf(...values)`, `.nullable()`, `.notNullable()`, `.default(value)` |
129
- | `number()` | Numeric value with constraints. | `.min(n)`, `.max(n)`, `.integer()`, `.positive()`, `.negative()`, `.finite()`, `.multipleOf(n)`, `.oneOf(...values)`, `.nullable()`, `.notNullable()`, `.default(value)` |
130
- | `boolean()` | Boolean value. | `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
131
- | `date()` | JavaScript `Date` instance. | `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
132
- | `func()` | Function value. | `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
129
+ | `number()` | Numeric value with constraints. | `.min(n)`, `.max(n)`, `.integer()`, `.positive()`, `.negative()`, `.finite()`, `.multipleOf(n)`, `.coerce()`, `.oneOf(...values)`, `.nullable()`, `.notNullable()`, `.default(value)` |
130
+ | `boolean()` | Boolean value. | `.coerce()`, `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
131
+ | `date()` | JavaScript `Date` instance. | `.coerce()`, `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
132
+ | `func()` | Function value. Supports typed parameter and return-type schemas. | `.addParameter(schema)`, `.hasReturnType(schema)`, `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
133
+ | `promise(schema?)` | JavaScript `Promise`. Optionally typed resolved value via `promise(schema)` or `.hasResolvedType(schema)`. | `.hasResolvedType(schema)`, `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
133
134
  | `nul()` | Exactly `null`. Useful in nullable unions. | `.optional()`, `.default(value)` |
134
135
  | `object(props)` | Object with typed properties. Supports nesting. | `.validate(data)`, `.addProps({...})`, `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
135
136
  | `array()` | Array with optional element schema (via `.of()`). | `.minLength(n)`, `.maxLength(n)`, `.of(schema)`, `.nonempty()`, `.unique()`, `.nullable()`, `.notNullable()`, `.default(value)` |
@@ -138,6 +139,8 @@ The following builder functions are available:
138
139
  | `union(schema)` | Union of schemas — e.g. `string \| number`. | `.or(schema)`, `.validate(data)`, `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
139
140
  | `enumOf(...values)` | String enum — sugar for `string().oneOf(...)`. | `.optional()`, `.nullable()`, `.notNullable()`, `.default(value)` |
140
141
  | `lazy(getter)` | Recursive/self-referential schema. The getter is called once and its result is cached. Enables tree structures, linked lists, and other recursive types. | `.resolve()`, `.optional()`, `.addValidator(fn)`, `.default(value)` |
142
+ | `generic(fn)` | Parameterized schema template. Call `.apply(...schemas)` with concrete schemas to obtain a fully typed concrete schema builder. TypeScript infers the result type from the template function's own generic signature. Optionally pass a `defaults` array as the first argument to enable direct validation without calling `.apply()`. | `.apply(...schemas)`, `.optional()`, `.nullable()`, `.default(value)` |
143
+ | `parseString(objectSchema, templateFn)` | Validates a string against a template pattern and parses it into a strongly-typed object. Property schemas handle their own coercion (e.g. `number().coerce()`). | `.optional()`, `.nullable()`, `.default(value)`, `.readonly()`, `.brand()` |
141
144
 
142
145
  ## Immutability
143
146
 
@@ -198,6 +201,224 @@ const IdOrEmail = union(string().minLength(1)).or(
198
201
  );
199
202
  ```
200
203
 
204
+ ## Generic Schemas
205
+
206
+ [▶ Open in Playground](https://docs.cleverbrush.com/playground/generic-basic)
207
+
208
+ Use `generic(fn)` to create a reusable, parameterized schema template. The template function accepts one or more schema builders as arguments and returns a concrete schema. Call `.apply(...schemas)` to instantiate the template — TypeScript infers the resulting type automatically from the function's own generic signature.
209
+
210
+ **Single type parameter** — a paginated list that works for any element type:
211
+
212
+ ```typescript
213
+ import {
214
+ generic, object, array, number, string,
215
+ type SchemaBuilder, type InferType
216
+ } from '@cleverbrush/schema';
217
+
218
+ const PaginatedList = generic(
219
+ <T extends SchemaBuilder<any, any, any, any, any>>(itemSchema: T) =>
220
+ object({
221
+ items: array(itemSchema),
222
+ total: number(),
223
+ page: number(),
224
+ })
225
+ );
226
+
227
+ const PaginatedUsers = PaginatedList.apply(
228
+ object({ name: string(), age: number() })
229
+ );
230
+
231
+ type PaginatedUsersType = InferType<typeof PaginatedUsers>;
232
+ // → { items: { name: string; age: number }[]; total: number; page: number }
233
+
234
+ PaginatedUsers.validate({
235
+ items: [{ name: 'Alice', age: 30 }],
236
+ total: 1,
237
+ page: 1,
238
+ }); // { valid: true }
239
+ ```
240
+
241
+ **Multiple type parameters** — a Result / Either type:
242
+
243
+ ```typescript
244
+ import {
245
+ generic, object, boolean, string, number,
246
+ type SchemaBuilder, type InferType
247
+ } from '@cleverbrush/schema';
248
+
249
+ const Result = generic(
250
+ <
251
+ T extends SchemaBuilder<any, any, any, any, any>,
252
+ E extends SchemaBuilder<any, any, any, any, any>
253
+ >(
254
+ valueSchema: T,
255
+ errorSchema: E
256
+ ) =>
257
+ object({
258
+ ok: boolean(),
259
+ value: valueSchema.optional(),
260
+ error: errorSchema.optional(),
261
+ })
262
+ );
263
+
264
+ const StringResult = Result.apply(string(), number());
265
+ // InferType → { ok: boolean; value?: string; error?: number }
266
+
267
+ StringResult.validate({ ok: true, value: 'hello' }); // valid
268
+ StringResult.validate({ ok: false, error: 404 }); // valid
269
+ ```
270
+
271
+ **Default arguments** — pass a `defaults` array as the first argument so the template can be validated directly without calling `.apply()` first:
272
+
273
+ ```typescript
274
+ import {
275
+ generic, object, array, number, any,
276
+ type SchemaBuilder
277
+ } from '@cleverbrush/schema';
278
+
279
+ const AnyList = generic(
280
+ [any()], // default for the single type parameter
281
+ <T extends SchemaBuilder<any, any, any, any, any>>(itemSchema: T) =>
282
+ object({ items: array(itemSchema), total: number() })
283
+ );
284
+
285
+ // Validate directly — uses the any() default
286
+ AnyList.validate({ items: [1, 'two', true], total: 3 }); // valid
287
+
288
+ // Or apply a stricter schema first
289
+ AnyList.apply(string()).validate({ items: ['a', 'b'], total: 2 }); // valid
290
+ ```
291
+
292
+ > **Tip:** Each call to `.apply()` returns an independent schema builder. You can chain `.optional()`, `.addValidator()`, `.default(value)`, and every other fluent method on the result.
293
+
294
+ ## Function Schemas
295
+
296
+ [▶ Open in Playground](https://docs.cleverbrush.com/playground/function-schema)
297
+
298
+ Use `func()` to validate that a value is a function. Two fluent methods let you annotate the expected signature — the inferred TypeScript type reflects both the parameter types and the return type:
299
+
300
+ - **`.addParameter(schema)`** — appends a parameter schema. Each call extends the inferred tuple of parameter types. The schemas are accessible at runtime via `introspect().parameters`.
301
+ - **`.hasReturnType(schema)`** — sets the return type schema. The inferred function type gains a concrete return type instead of `any`. Accessible via `introspect().returnType`.
302
+
303
+ ```typescript
304
+ import { func, string, number, boolean, InferType } from '@cleverbrush/schema';
305
+
306
+ // Plain function validator — accepts any () => any
307
+ const anyFn = func();
308
+ anyFn.validate(() => 42); // { valid: true }
309
+ anyFn.validate('not a fn'); // { valid: false }
310
+
311
+ // Typed parameter and return type
312
+ const greet = func()
313
+ .addParameter(string()) // first param: string
314
+ .addParameter(number().optional()) // second param: number | undefined
315
+ .hasReturnType(string()); // return type: string
316
+
317
+ // InferType preserves the declared parameter types and return type.
318
+ // Function schemas also remain compatible with callbacks of any arity.
319
+ type Greet = InferType<typeof greet>;
320
+ // → (param0: string, param1: number | undefined, ...args: any[]) => string
321
+
322
+ // Introspect at runtime
323
+ const info = greet.introspect();
324
+ // info.parameters → [StringSchemaBuilder, NumberSchemaBuilder]
325
+ // info.returnType → StringSchemaBuilder
326
+
327
+ // Optional function schemas infer a union with undefined
328
+ const optionalHandler = func()
329
+ .addParameter(string())
330
+ .addParameter(boolean())
331
+ .hasReturnType(boolean())
332
+ .optional();
333
+
334
+ type OptionalHandler = InferType<typeof optionalHandler>;
335
+ // → ((param0: string, param1: boolean, ...args: any[]) => boolean) | undefined
336
+ ```
337
+
338
+ ## Constructor Schemas
339
+
340
+ [▶ Open in Playground](https://docs.cleverbrush.com/playground/constructor-schema)
341
+
342
+ Use `.addConstructor(funcSchema)` on an `object()` schema to declare one or more constructor overloads. Each call appends a `FunctionSchemaBuilder` to an accumulated list; the inferred TypeScript type becomes an intersection of all construct signatures and the plain instance type.
343
+
344
+ - **`.addConstructor(funcSchema)`** — appends a constructor overload. Each call extends the inferred tuple of constructor schemas. Chainable.
345
+ - **`.clearConstructors()`** — resets constructor schemas to an empty list, removing all construct signatures from the inferred type.
346
+ - **`introspect().constructorSchemas`** — array of all accumulated constructor `FunctionSchemaBuilder` schemas.
347
+
348
+ Constructor signatures are **type-only**: runtime `validate()` continues to validate plain objects as before.
349
+
350
+ ```typescript
351
+ import { object, func, string, number, InferType } from '@cleverbrush/schema';
352
+
353
+ // Single constructor overload
354
+ const PersonSchema = object({ name: string(), age: number() })
355
+ .addConstructor(func().addParameter(string()));
356
+
357
+ type Person = InferType<typeof PersonSchema>;
358
+ // → { new(p0: string): { name: string; age: number } } & { name: string; age: number }
359
+
360
+ // Multiple chained constructors → overloaded construct signatures
361
+ const FlexPersonSchema = object({ name: string(), age: number() })
362
+ .addConstructor(func().addParameter(string()))
363
+ .addConstructor(func().addParameter(string()).addParameter(number()));
364
+
365
+ type FlexPerson = InferType<typeof FlexPersonSchema>;
366
+ // → { new(p0: string): { name: string; age: number } }
367
+ // & { new(p0: string, p1: number): { name: string; age: number } }
368
+ // & { name: string; age: number }
369
+
370
+ // Runtime validation is unchanged — plain objects still validate
371
+ FlexPersonSchema.validate({ name: 'Alice', age: 30 }); // { valid: true }
372
+
373
+ // Introspect constructor schemas at runtime
374
+ const { constructorSchemas } = FlexPersonSchema.introspect();
375
+ // constructorSchemas.length → 2
376
+
377
+ // Remove all constructor signatures
378
+ const PlainSchema = FlexPersonSchema.clearConstructors();
379
+ type Plain = InferType<typeof PlainSchema>;
380
+ // → { name: string; age: number }
381
+ ```
382
+
383
+ ## Promise Schemas
384
+
385
+ [▶ Open in Playground](https://docs.cleverbrush.com/playground/promise-schema)
386
+
387
+ Use `promise()` to validate that a value is a JavaScript `Promise`. Pass an optional schema to type the resolved value — the inferred TypeScript type becomes `Promise<T>`. You can also call `.hasResolvedType(schema)` on the returned builder to set or replace the resolved-value schema.
388
+
389
+ - **`promise(schema?)`** — factory shorthand. When `schema` is provided the inferred type is `Promise<InferType<typeof schema>>`.
390
+ - **`.hasResolvedType(schema)`** — sets the resolved-value schema; replaces any previously set one. Accessible via `introspect().resolvedType`.
391
+
392
+ ```typescript
393
+ import { promise, string, number, InferType } from '@cleverbrush/schema';
394
+
395
+ // Untyped — validates that the value is any Promise
396
+ const anyPromise = promise();
397
+ anyPromise.validate(Promise.resolve(42)); // { valid: true }
398
+ anyPromise.validate('not a promise' as any); // { valid: false }
399
+
400
+ // Typed resolved value via factory argument
401
+ const stringPromise = promise(string());
402
+ type StringPromise = InferType<typeof stringPromise>;
403
+ // → Promise<string>
404
+
405
+ // Typed resolved value via fluent method
406
+ const numPromise = promise().hasResolvedType(number());
407
+ type NumPromise = InferType<typeof numPromise>;
408
+ // → Promise<number>
409
+
410
+ // Introspect at runtime
411
+ const info = numPromise.introspect();
412
+ // info.resolvedType → NumberSchemaBuilder
413
+
414
+ // Optional promise — undefined is also accepted
415
+ const optPromise = promise(string()).optional();
416
+ type OptPromise = InferType<typeof optPromise>;
417
+ // → Promise<string> | undefined
418
+
419
+ optPromise.validate(undefined as any); // { valid: true }
420
+ ```
421
+
201
422
  ## Record Schemas
202
423
 
203
424
  [▶ Open in Playground](https://docs.cleverbrush.com/playground/record-basics)
@@ -262,6 +483,86 @@ if (!result.valid) {
262
483
  }
263
484
  ```
264
485
 
486
+ ## Parse String Schemas
487
+
488
+ [▶ Open in Playground](https://docs.cleverbrush.com/playground/parse-string-basic)
489
+
490
+ Use `parseString(objectSchema, templateFn)` to validate a string against a template pattern and parse it into a strongly-typed object. The template expression uses a tagged-template syntax with type-safe property selectors — you get full IntelliSense in the selector lambdas.
491
+
492
+ ```typescript
493
+ import { parseString, object, string, number, type InferType } from '@cleverbrush/schema';
494
+
495
+ const RouteSchema = parseString(
496
+ object({ userId: string().uuid(), id: number().coerce() }),
497
+ $t => $t`/orders/${t => t.id}/${t => t.userId}`
498
+ );
499
+
500
+ type Route = InferType<typeof RouteSchema>;
501
+ // { userId: string; id: number }
502
+
503
+ const result = RouteSchema.validate('/orders/42/550e8400-e29b-41d4-a716-446655440000');
504
+ // result.valid === true
505
+ // result.object === { id: 42, userId: '550e8400-...' }
506
+ ```
507
+
508
+ **Nested objects** — navigate deep properties via `t => t.parent.child`:
509
+
510
+ ```typescript
511
+ const schema = parseString(
512
+ object({
513
+ order: object({ id: number().coerce() }),
514
+ user: object({ name: string() })
515
+ }),
516
+ $t => $t`/orders/${t => t.order.id}/by/${t => t.user.name}`
517
+ );
518
+ // InferType → { order: { id: number }; user: { name: string } }
519
+
520
+ schema.validate('/orders/42/by/Alice');
521
+ // { valid: true, object: { order: { id: 42 }, user: { name: 'Alice' } } }
522
+ ```
523
+
524
+ **Coercion** is the property schema's responsibility — the builder passes the raw captured substring directly to each property schema's `validate()`. Use `.coerce()` on `number()`, `boolean()`, or `date()` to convert from strings:
525
+
526
+ ```typescript
527
+ const LogEntry = parseString(
528
+ object({
529
+ level: string(),
530
+ ts: date().coerce(),
531
+ message: string()
532
+ }),
533
+ $t => $t`[${t => t.level}] ${t => t.ts} ${t => t.message}`
534
+ );
535
+ ```
536
+
537
+ Error messages include the property path for easy debugging:
538
+
539
+ ```typescript
540
+ const result = RouteSchema.validate('/orders/abc/bad-uuid');
541
+ // result.errors[0].message → "id: expected an integer number"
542
+ ```
543
+
544
+ ## Coercion
545
+
546
+ The `number()`, `boolean()`, and `date()` builders each have a `.coerce()` method that adds a preprocessor to convert string values to the target type. This is especially useful with parse-string schemas where captured segments are always strings, but also works standalone for URL parameters, form inputs, or any other string source.
547
+
548
+ ```typescript
549
+ import { number, boolean, date } from '@cleverbrush/schema';
550
+
551
+ // number().coerce() — uses Number(value)
552
+ number().coerce().validate('42'); // { valid: true, object: 42 }
553
+ number().coerce().validate('hello'); // { valid: false } — NaN fails
554
+
555
+ // boolean().coerce() — "true" → true, "false" → false
556
+ boolean().coerce().validate('true'); // { valid: true, object: true }
557
+ boolean().coerce().validate('yes'); // { valid: false } — unrecognized
558
+
559
+ // date().coerce() — new Date(value) if valid
560
+ date().coerce().validate('2024-01-15'); // { valid: true, object: Date }
561
+ date().coerce().validate('nope'); // { valid: false } — invalid date
562
+ ```
563
+
564
+ Non-string values pass through unchanged, so `.coerce()` is safe to chain even when the input might already be the correct type. All three methods return a new immutable schema instance.
565
+
265
566
  ## Recursive Schemas
266
567
 
267
568
  [▶ Open in Playground](https://docs.cleverbrush.com/playground/recursive-schemas)
@@ -596,6 +897,43 @@ const SignupSchema = object({
596
897
  });
597
898
  ```
598
899
 
900
+ ### Property-Targeted Validator Errors
901
+
902
+ By default, errors returned from object-level validators are attached to the root object. You can target an error to a specific property by providing a `property` selector — the same selector used by `getErrorsFor()` and react-form's `forProperty`:
903
+
904
+ ```typescript
905
+ const SignupSchema = object({
906
+ password: string().minLength(8),
907
+ confirmPassword: string().minLength(8)
908
+ }).addValidator((value) => {
909
+ if (value.password !== value.confirmPassword) {
910
+ return {
911
+ valid: false,
912
+ errors: [{
913
+ message: 'Passwords do not match',
914
+ property: (t) => t.confirmPassword
915
+ }]
916
+ };
917
+ }
918
+ return { valid: true };
919
+ });
920
+
921
+ const result = SignupSchema.validate(
922
+ { password: 'secret1', confirmPassword: 'secret2' },
923
+ { doNotStopOnFirstError: true }
924
+ );
925
+
926
+ // Error is routed to confirmPassword:
927
+ result.getErrorsFor((t) => t.confirmPassword).errors;
928
+ // → ['Passwords do not match']
929
+
930
+ // Other properties are unaffected:
931
+ result.getErrorsFor((t) => t.password).errors;
932
+ // → []
933
+ ```
934
+
935
+ You can target multiple properties from a single validator by returning multiple errors with different `property` selectors. Errors without a `property` selector are attached to the root object as before.
936
+
599
937
  ### Per-Property Errors with `getErrorsFor()` (Recommended)
600
938
 
601
939
  `ObjectSchemaBuilder.validate()` returns an extended result with a `getErrorsFor()` method for inspecting errors on individual properties — perfect for showing inline form errors. **This is the recommended way to inspect validation errors on object schemas** and replaces the deprecated `errors` array on `ObjectSchemaValidationResult`:
@@ -700,6 +1038,28 @@ console.log(info.hasDefault); // true
700
1038
  console.log(info.defaultValue); // 'hello'
701
1039
  ```
702
1040
 
1041
+ ## Examples
1042
+
1043
+ Attach example values to any schema with `.example(value)`. The example is typed to the schema's result type, so the compiler catches mismatches. Examples are purely metadata — validation is unaffected.
1044
+
1045
+ ```typescript
1046
+ import { string, number, object } from '@cleverbrush/schema';
1047
+
1048
+ const Email = string().example('user@example.com');
1049
+ const Age = number().example(30);
1050
+
1051
+ const User = object({
1052
+ email: Email,
1053
+ age: Age,
1054
+ }).example({ email: 'alice@example.com', age: 25 });
1055
+ ```
1056
+
1057
+ Examples are exposed via `.introspect()` and consumed by `@cleverbrush/schema-json` (emitted as the JSON Schema `examples` keyword) and `@cleverbrush/server-openapi` (pre-fills parameter and response schemas in generated specs).
1058
+
1059
+ ```typescript
1060
+ Email.introspect().example; // 'user@example.com'
1061
+ ```
1062
+
703
1063
  ## Catch / Fallback
704
1064
 
705
1065
  Every schema builder supports `.catch(value)`. When validation **fails for any reason** — wrong type, constraint violation, missing required value — the fallback is returned as a successful result instead of errors.
@@ -790,6 +1150,50 @@ console.log(schema.introspect().isReadonly); // true
790
1150
 
791
1151
  > **Note:** `.readonly()` is **shallow** — only top-level object properties or the array itself are marked readonly. For deeply nested immutability consider applying `.readonly()` at each level, or use a `DeepReadonly` utility type post-validation.
792
1152
 
1153
+ ## schemaName
1154
+
1155
+ Every schema builder supports `.schemaName(name)`. This is a **metadata-only** modifier — it attaches a component name to the schema for use by OpenAPI tooling. It has no effect on validation or type inference.
1156
+
1157
+ ```typescript
1158
+ import { object, string, number } from '@cleverbrush/schema';
1159
+
1160
+ export const UserSchema = object({
1161
+ id: number(),
1162
+ name: string(),
1163
+ }).schemaName('User');
1164
+
1165
+ // Accessible at runtime
1166
+ UserSchema.introspect().schemaName; // 'User'
1167
+ ```
1168
+
1169
+ Chains naturally with all other modifiers:
1170
+
1171
+ ```typescript
1172
+ const ProductSchema = object({
1173
+ sku: string().nonempty(),
1174
+ price: number().min(0),
1175
+ })
1176
+ .schemaName('Product')
1177
+ .describe('A product in the catalogue');
1178
+ ```
1179
+
1180
+ When used with [`@cleverbrush/server-openapi`](../server-openapi), any schema that carries a `schemaName` is automatically extracted into `components/schemas` and all usages in the document are replaced with `$ref` pointers — eliminating repeated inline definitions:
1181
+
1182
+ ```typescript
1183
+ import { generateOpenApiSpec } from '@cleverbrush/server-openapi';
1184
+
1185
+ // UserSchema is emitted once under components.schemas.User
1186
+ // Every endpoint that references it gets: { $ref: '#/components/schemas/User' }
1187
+ generateOpenApiSpec({ registrations, info: { title: 'My API', version: '1.0.0' } });
1188
+ ```
1189
+
1190
+ > **Name uniqueness:** Registering two *different* schema instances under the same name throws an error. Always export named schemas as constants and reuse the same reference everywhere.
1191
+
1192
+ | Method / Property | Signature | Notes |
1193
+ |---|---|---|
1194
+ | `.schemaName(name)` | `schemaName(name: string): this` | Returns a new builder; original is unchanged |
1195
+ | `.introspect().schemaName` | `string \| undefined` | The name passed to `.schemaName()`, or `undefined` |
1196
+
793
1197
  ## Describe
794
1198
 
795
1199
  Every schema builder supports `.describe(text)`. This is a **metadata-only** modifier — it stores a human-readable description on the schema at runtime with no effect on validation.
@@ -1255,9 +1659,9 @@ Define a schema once and use it for runtime validation, object mapping between d
1255
1659
 
1256
1660
  ## Exports
1257
1661
 
1258
- **Builder functions:** `any`, `lazy`, `string`, `number`, `boolean`, `func`, `object`, `date`, `array`, `union`
1662
+ **Builder functions:** `any`, `lazy`, `string`, `number`, `boolean`, `func`, `promise`, `object`, `date`, `array`, `union`, `parseString`
1259
1663
 
1260
- **Builder classes** (for extending): `SchemaBuilder`, `AnySchemaBuilder`, `ArraySchemaBuilder`, `BooleanSchemaBuilder`, `DateSchemaBuilder`, `FunctionSchemaBuilder`, `LazySchemaBuilder`, `NumberSchemaBuilder`, `ObjectSchemaBuilder`, `StringSchemaBuilder`, `UnionSchemaBuilder`
1664
+ **Builder classes** (for extending): `SchemaBuilder`, `AnySchemaBuilder`, `ArraySchemaBuilder`, `BooleanSchemaBuilder`, `DateSchemaBuilder`, `FunctionSchemaBuilder`, `ParseStringSchemaBuilder`, `LazySchemaBuilder`, `NumberSchemaBuilder`, `ObjectSchemaBuilder`, `PromiseSchemaBuilder`, `StringSchemaBuilder`, `UnionSchemaBuilder`
1261
1665
 
1262
1666
  **Extension system:** `defineExtension`, `withExtensions`, `stringExtensions`, `numberExtensions`, `arrayExtensions`, `nullableExtension`
1263
1667
 
@@ -1,2 +1,2 @@
1
- import{a,b}from"../chunk-ZAN4ZHCJ.js";import"../chunk-CTP4RHDG.js";export{a as AnySchemaBuilder,b as any};
1
+ import{a,b}from"../chunk-HN774HD7.js";import"../chunk-3JMDGYDT.js";export{a as AnySchemaBuilder,b as any};
2
2
  //# sourceMappingURL=AnySchemaBuilder.js.map
@@ -147,8 +147,10 @@ export declare class ArraySchemaBuilder<TElementSchema extends SchemaBuilder<any
147
147
  hasDefault: boolean;
148
148
  defaultValue: TResult | (() => TResult) | undefined;
149
149
  description: string | undefined;
150
+ schemaName: string | undefined;
150
151
  hasCatch: boolean;
151
152
  catchValue: TResult | (() => TResult) | undefined;
153
+ example: unknown;
152
154
  };
153
155
  /**
154
156
  * Set a schema that every array item has to satisfy. If it is not set,
@@ -1,2 +1,2 @@
1
- import{a,b}from"../chunk-Z7QZ7IXT.js";import"../chunk-CTP4RHDG.js";export{a as ArraySchemaBuilder,b as array};
1
+ import{a,b}from"../chunk-NUW3VXZV.js";import"../chunk-3JMDGYDT.js";export{a as ArraySchemaBuilder,b as array};
2
2
  //# sourceMappingURL=ArraySchemaBuilder.js.map
@@ -60,8 +60,10 @@ export declare class BooleanSchemaBuilder<TResult = boolean, TRequired extends b
60
60
  hasDefault: boolean;
61
61
  defaultValue: TFinalResult | (() => TFinalResult) | undefined;
62
62
  description: string | undefined;
63
+ schemaName: string | undefined;
63
64
  hasCatch: boolean;
64
65
  catchValue: TFinalResult | (() => TFinalResult) | undefined;
66
+ example: unknown;
65
67
  };
66
68
  /**
67
69
  * @inheritdoc
@@ -130,6 +132,19 @@ export declare class BooleanSchemaBuilder<TResult = boolean, TRequired extends b
130
132
  * Removes a `value` defined by `equals()` call.
131
133
  */
132
134
  clearEquals(): BooleanSchemaBuilder<boolean, TRequired, TNullable, TExplicitType, THasDefault, TExtensions> & TExtensions;
135
+ /**
136
+ * Adds a preprocessor that coerces a string value to a boolean.
137
+ * Accepts `"true"` → `true` and `"false"` → `false`; other values are
138
+ * left unchanged so the boolean schema rejects them.
139
+ *
140
+ * @example ```ts
141
+ * const schema = boolean().coerce();
142
+ * const result = schema.validate('true');
143
+ * // result.valid === true
144
+ * // result.object === true
145
+ * ```
146
+ */
147
+ coerce(): BooleanSchemaBuilder<TResult, TRequired, TNullable, TExplicitType, THasDefault, TExtensions> & TExtensions;
133
148
  /**
134
149
  * @hidden
135
150
  */
@@ -1,2 +1,2 @@
1
- import{a,b}from"../chunk-KFTO7TMB.js";import"../chunk-CTP4RHDG.js";export{a as BooleanSchemaBuilder,b as boolean};
1
+ import{a,b}from"../chunk-CFIJQ4GP.js";import"../chunk-3JMDGYDT.js";export{a as BooleanSchemaBuilder,b as boolean};
2
2
  //# sourceMappingURL=BooleanSchemaBuilder.js.map
@@ -122,8 +122,10 @@ export declare class DateSchemaBuilder<TResult = Date, TRequired extends boolean
122
122
  hasDefault: boolean;
123
123
  defaultValue: TResult | (() => TResult) | undefined;
124
124
  description: string | undefined;
125
+ schemaName: string | undefined;
125
126
  hasCatch: boolean;
126
127
  catchValue: TResult | (() => TResult) | undefined;
128
+ example: unknown;
127
129
  };
128
130
  /**
129
131
  * @inheritdoc
@@ -243,6 +245,24 @@ export declare class DateSchemaBuilder<TResult = Date, TRequired extends boolean
243
245
  * Clear `max()` call.
244
246
  */
245
247
  clearMax(): DateSchemaBuilder<TResult, TRequired, TNullable, THasDefault, TExtensions> & TExtensions;
248
+ /**
249
+ * Adds a preprocessor that coerces a string value to a Date
250
+ * using `new Date(value)`. If the string does not represent a
251
+ * valid date the original value is left unchanged so the date
252
+ * schema rejects it.
253
+ *
254
+ * This is a convenient alias for common string→Date coercion.
255
+ * For more specific parsing, see {@link acceptJsonString} and
256
+ * {@link acceptEpoch}.
257
+ *
258
+ * @example ```ts
259
+ * const schema = date().coerce();
260
+ * const result = schema.validate('2024-01-15');
261
+ * // result.valid === true
262
+ * // result.object instanceof Date === true
263
+ * ```
264
+ */
265
+ coerce(): DateSchemaBuilder<TResult, TRequired, TNullable, THasDefault, TExtensions> & TExtensions;
246
266
  /**
247
267
  * Accepts JSON string as a valid Date.
248
268
  * String must be in ISO format and will be parsed using `JSON.parse()`.
@@ -1,2 +1,2 @@
1
- import{a,b}from"../chunk-YWBNVHR6.js";import"../chunk-CTP4RHDG.js";export{a as DateSchemaBuilder,b as date};
1
+ import{a,b}from"../chunk-EIVZX4ZO.js";import"../chunk-3JMDGYDT.js";export{a as DateSchemaBuilder,b as date};
2
2
  //# sourceMappingURL=DateSchemaBuilder.js.map
@@ -107,8 +107,10 @@ export declare class ExternSchemaBuilder<TStandardSchema extends StandardSchemaV
107
107
  hasDefault: boolean;
108
108
  defaultValue: TResult | (() => TResult) | undefined;
109
109
  description: string | undefined;
110
+ schemaName: string | undefined;
110
111
  hasCatch: boolean;
111
112
  catchValue: TResult | (() => TResult) | undefined;
113
+ example: unknown;
112
114
  };
113
115
  /**
114
116
  * @hidden
@@ -1,2 +1,2 @@
1
- import{a,b}from"../chunk-MKAYFPAR.js";import"../chunk-CTP4RHDG.js";export{a as ExternSchemaBuilder,b as extern};
1
+ import{a,b}from"../chunk-PHE4LIAN.js";import"../chunk-3JMDGYDT.js";export{a as ExternSchemaBuilder,b as extern};
2
2
  //# sourceMappingURL=ExternSchemaBuilder.js.map