@warlock.js/seal 5.30.0 → 5.32.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/CHANGELOG.md +46 -7
- package/README.md +57 -9
- package/cjs/index.cjs +41 -29
- package/cjs/index.cjs.map +1 -1
- package/cjs/{object--LXRt5RF.cjs → object-BoVGJ68E.cjs} +43 -24
- package/cjs/object-BoVGJ68E.cjs.map +1 -0
- package/cjs/object.cjs +2 -2
- package/cjs/{required-methods-DYsRO9aZ.cjs → required-methods-BJlnLnwa.cjs} +343 -258
- package/cjs/required-methods-BJlnLnwa.cjs.map +1 -0
- package/cjs/{string-DsqnKoSB.cjs → string-CM8x0u2w.cjs} +46 -44
- package/cjs/string-CM8x0u2w.cjs.map +1 -0
- package/cjs/string.cjs +2 -2
- package/esm/config.d.mts +7 -8
- package/esm/config.d.mts.map +1 -1
- package/esm/config.mjs.map +1 -1
- package/esm/factory/index.d.mts +2 -1
- package/esm/factory/index.mjs +1 -1
- package/esm/factory/validate.d.mts +3 -6
- package/esm/factory/validate.d.mts.map +1 -1
- package/esm/factory/validators.d.mts +16 -13
- package/esm/factory/validators.d.mts.map +1 -1
- package/esm/factory/validators.mjs +7 -1
- package/esm/factory/validators.mjs.map +1 -1
- package/esm/helpers/date-helpers.d.mts +1 -2
- package/esm/helpers/date-helpers.d.mts.map +1 -1
- package/esm/helpers/file.utils.d.mts +2 -4
- package/esm/helpers/file.utils.d.mts.map +1 -1
- package/esm/helpers/get-field-value.d.mts +2 -3
- package/esm/helpers/get-field-value.d.mts.map +1 -1
- package/esm/helpers/get-field-value.mjs +2 -1
- package/esm/helpers/get-field-value.mjs.map +1 -1
- package/esm/helpers/index.d.mts +6 -0
- package/esm/helpers/index.mjs +1 -1
- package/esm/helpers/path-helpers.d.mts +1 -2
- package/esm/helpers/path-helpers.d.mts.map +1 -1
- package/esm/helpers/validation-helpers.d.mts +4 -10
- package/esm/helpers/validation-helpers.d.mts.map +1 -1
- package/esm/index.d.mts +7 -0
- package/esm/mutators/array-mutators.d.mts +6 -7
- package/esm/mutators/array-mutators.d.mts.map +1 -1
- package/esm/mutators/boolean-mutators.d.mts +3 -4
- package/esm/mutators/boolean-mutators.d.mts.map +1 -1
- package/esm/mutators/date-mutators.d.mts +18 -19
- package/esm/mutators/date-mutators.d.mts.map +1 -1
- package/esm/mutators/index.d.mts +8 -0
- package/esm/mutators/index.mjs +1 -1
- package/esm/mutators/number-mutators.d.mts +10 -11
- package/esm/mutators/number-mutators.d.mts.map +1 -1
- package/esm/mutators/object-mutators.d.mts +4 -5
- package/esm/mutators/object-mutators.d.mts.map +1 -1
- package/esm/mutators/object-mutators.mjs +5 -4
- package/esm/mutators/object-mutators.mjs.map +1 -1
- package/esm/mutators/scalar-mutators.d.mts +2 -3
- package/esm/mutators/scalar-mutators.d.mts.map +1 -1
- package/esm/mutators/string-mutators.d.mts +37 -38
- package/esm/mutators/string-mutators.d.mts.map +1 -1
- package/esm/mutators/string-mutators.mjs +5 -3
- package/esm/mutators/string-mutators.mjs.map +1 -1
- package/esm/object.d.mts +3 -5
- package/esm/object.d.mts.map +1 -1
- package/esm/object.mjs.map +1 -1
- package/esm/plugins/plugin-system.d.mts +11 -9
- package/esm/plugins/plugin-system.d.mts.map +1 -1
- package/esm/rules/array/array-rules.d.mts +3 -4
- package/esm/rules/array/array-rules.d.mts.map +1 -1
- package/esm/rules/array/index.d.mts +2 -0
- package/esm/rules/array/index.mjs +1 -1
- package/esm/rules/color/color-rules.d.mts +8 -9
- package/esm/rules/color/color-rules.d.mts.map +1 -1
- package/esm/rules/color/index.d.mts +2 -0
- package/esm/rules/color/index.mjs +1 -1
- package/esm/rules/common/enum.d.mts +5 -6
- package/esm/rules/common/enum.d.mts.map +1 -1
- package/esm/rules/common/equals-field-rules.d.mts +3 -4
- package/esm/rules/common/equals-field-rules.d.mts.map +1 -1
- package/esm/rules/common/index.d.mts +7 -0
- package/esm/rules/common/index.mjs +1 -1
- package/esm/rules/common/instanceof.d.mts +2 -3
- package/esm/rules/common/instanceof.d.mts.map +1 -1
- package/esm/rules/common/literal.d.mts +2 -3
- package/esm/rules/common/literal.d.mts.map +1 -1
- package/esm/rules/common/literal.mjs.map +1 -1
- package/esm/rules/common/type-rules.d.mts +10 -11
- package/esm/rules/common/type-rules.d.mts.map +1 -1
- package/esm/rules/common/unknown-key.d.mts +2 -3
- package/esm/rules/common/unknown-key.d.mts.map +1 -1
- package/esm/rules/conditional/forbidden-if-rules.d.mts +7 -8
- package/esm/rules/conditional/forbidden-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/index.d.mts +11 -0
- package/esm/rules/conditional/index.mjs +1 -1
- package/esm/rules/conditional/present-if-rules.d.mts +6 -7
- package/esm/rules/conditional/present-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-unless-rules.d.mts +2 -3
- package/esm/rules/conditional/present-unless-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-with-rules.d.mts +4 -5
- package/esm/rules/conditional/present-with-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-without-rules.d.mts +4 -5
- package/esm/rules/conditional/present-without-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-if-rules.d.mts +10 -11
- package/esm/rules/conditional/required-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-unless-rules.d.mts +2 -3
- package/esm/rules/conditional/required-unless-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-when-rule.d.mts +2 -3
- package/esm/rules/conditional/required-when-rule.d.mts.map +1 -1
- package/esm/rules/conditional/required-with-rules.d.mts +4 -5
- package/esm/rules/conditional/required-with-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-without-rules.d.mts +4 -5
- package/esm/rules/conditional/required-without-rules.d.mts.map +1 -1
- package/esm/rules/core/equal.d.mts +2 -3
- package/esm/rules/core/equal.d.mts.map +1 -1
- package/esm/rules/core/forbidden.d.mts +2 -3
- package/esm/rules/core/forbidden.d.mts.map +1 -1
- package/esm/rules/core/index.d.mts +6 -0
- package/esm/rules/core/index.mjs +1 -1
- package/esm/rules/core/required.d.mts +3 -4
- package/esm/rules/core/required.d.mts.map +1 -1
- package/esm/rules/core/union.d.mts +10 -4
- package/esm/rules/core/union.d.mts.map +1 -1
- package/esm/rules/core/union.mjs +56 -10
- package/esm/rules/core/union.mjs.map +1 -1
- package/esm/rules/core/when.d.mts +2 -3
- package/esm/rules/core/when.d.mts.map +1 -1
- package/esm/rules/core/when.mjs.map +1 -1
- package/esm/rules/date/date-comparison-rules.d.mts +6 -7
- package/esm/rules/date/date-comparison-rules.d.mts.map +1 -1
- package/esm/rules/date/date-day-rules.d.mts +5 -6
- package/esm/rules/date/date-day-rules.d.mts.map +1 -1
- package/esm/rules/date/date-field-comparison-rules.d.mts +4 -5
- package/esm/rules/date/date-field-comparison-rules.d.mts.map +1 -1
- package/esm/rules/date/date-field-comparison-rules.mjs +6 -3
- package/esm/rules/date/date-field-comparison-rules.mjs.map +1 -1
- package/esm/rules/date/date-period-rules.d.mts +16 -16
- package/esm/rules/date/date-period-rules.d.mts.map +1 -1
- package/esm/rules/date/date-period-rules.mjs +24 -12
- package/esm/rules/date/date-period-rules.mjs.map +1 -1
- package/esm/rules/date/date-relative-rules.d.mts +4 -5
- package/esm/rules/date/date-relative-rules.d.mts.map +1 -1
- package/esm/rules/date/date-relative-rules.mjs +3 -3
- package/esm/rules/date/date-relative-rules.mjs.map +1 -1
- package/esm/rules/date/date-special-rules.d.mts +4 -5
- package/esm/rules/date/date-special-rules.d.mts.map +1 -1
- package/esm/rules/date/date.d.mts +16 -17
- package/esm/rules/date/date.d.mts.map +1 -1
- package/esm/rules/date/date.mjs +4 -2
- package/esm/rules/date/date.mjs.map +1 -1
- package/esm/rules/date/index.d.mts +8 -0
- package/esm/rules/date/index.mjs +1 -1
- package/esm/rules/file/dimensions.d.mts +5 -6
- package/esm/rules/file/dimensions.d.mts.map +1 -1
- package/esm/rules/file/file-size.d.mts +3 -4
- package/esm/rules/file/file-size.d.mts.map +1 -1
- package/esm/rules/file/index.d.mts +3 -0
- package/esm/rules/file/index.mjs +1 -1
- package/esm/rules/index.d.mts +56 -0
- package/esm/rules/index.mjs +1 -1
- package/esm/rules/length/index.d.mts +2 -0
- package/esm/rules/length/index.mjs +1 -1
- package/esm/rules/length/length-rules.d.mts +8 -9
- package/esm/rules/length/length-rules.d.mts.map +1 -1
- package/esm/rules/number/index.d.mts +2 -0
- package/esm/rules/number/index.mjs +1 -1
- package/esm/rules/number/number-rules.d.mts +11 -12
- package/esm/rules/number/number-rules.d.mts.map +1 -1
- package/esm/rules/number/number-rules.mjs +12 -6
- package/esm/rules/number/number-rules.mjs.map +1 -1
- package/esm/rules/scalar/index.mjs +1 -1
- package/esm/rules/string/alpha.d.mts +4 -5
- package/esm/rules/string/alpha.d.mts.map +1 -1
- package/esm/rules/string/credit-card.d.mts +2 -3
- package/esm/rules/string/credit-card.d.mts.map +1 -1
- package/esm/rules/string/email.d.mts +2 -3
- package/esm/rules/string/email.d.mts.map +1 -1
- package/esm/rules/string/id-formats.d.mts +6 -7
- package/esm/rules/string/id-formats.d.mts.map +1 -1
- package/esm/rules/string/index.d.mts +12 -0
- package/esm/rules/string/index.mjs +1 -1
- package/esm/rules/string/ip.d.mts +7 -8
- package/esm/rules/string/ip.d.mts.map +1 -1
- package/esm/rules/string/matches.d.mts +2 -3
- package/esm/rules/string/matches.d.mts.map +1 -1
- package/esm/rules/string/pattern.d.mts +2 -3
- package/esm/rules/string/pattern.d.mts.map +1 -1
- package/esm/rules/string/string-comparison.d.mts +5 -6
- package/esm/rules/string/string-comparison.d.mts.map +1 -1
- package/esm/rules/string/strong-password-rule.d.mts +2 -3
- package/esm/rules/string/strong-password-rule.d.mts.map +1 -1
- package/esm/rules/string/url.d.mts +2 -3
- package/esm/rules/string/url.d.mts.map +1 -1
- package/esm/rules/string/without-whitespace.d.mts +2 -3
- package/esm/rules/string/without-whitespace.d.mts.map +1 -1
- package/esm/standard-schema/index.d.mts +4 -0
- package/esm/standard-schema/json-schema.d.mts +5 -7
- package/esm/standard-schema/json-schema.d.mts.map +1 -1
- package/esm/standard-schema/json-schema.mjs.map +1 -1
- package/esm/standard-schema/map-result.d.mts +2 -3
- package/esm/standard-schema/map-result.d.mts.map +1 -1
- package/esm/standard-schema/map-result.mjs.map +1 -1
- package/esm/standard-schema/types.d.mts +4 -5
- package/esm/standard-schema/types.d.mts.map +1 -1
- package/esm/string.d.mts +2 -3
- package/esm/string.d.mts.map +1 -1
- package/esm/types/conditional-types.d.mts +8 -6
- package/esm/types/conditional-types.d.mts.map +1 -1
- package/esm/types/context-types.d.mts +24 -15
- package/esm/types/context-types.d.mts.map +1 -1
- package/esm/types/data-transformer-types.d.mts +3 -5
- package/esm/types/data-transformer-types.d.mts.map +1 -1
- package/esm/types/date-types.d.mts +2 -3
- package/esm/types/date-types.d.mts.map +1 -1
- package/esm/types/file.types.d.mts +1 -2
- package/esm/types/file.types.d.mts.map +1 -1
- package/esm/types/index.d.mts +11 -0
- package/esm/types/inference-types.d.mts +15 -14
- package/esm/types/inference-types.d.mts.map +1 -1
- package/esm/types/mutator-types.d.mts +9 -7
- package/esm/types/mutator-types.d.mts.map +1 -1
- package/esm/types/result-types.d.mts +12 -7
- package/esm/types/result-types.d.mts.map +1 -1
- package/esm/types/rule-types.d.mts +33 -15
- package/esm/types/rule-types.d.mts.map +1 -1
- package/esm/types/schema-types.d.mts +1 -3
- package/esm/types/schema-types.d.mts.map +1 -1
- package/esm/validators/any-validator.d.mts +1 -3
- package/esm/validators/any-validator.d.mts.map +1 -1
- package/esm/validators/array-validator.d.mts +2 -3
- package/esm/validators/array-validator.d.mts.map +1 -1
- package/esm/validators/array-validator.mjs +3 -3
- package/esm/validators/array-validator.mjs.map +1 -1
- package/esm/validators/base-validator.d.mts +10 -11
- package/esm/validators/base-validator.d.mts.map +1 -1
- package/esm/validators/base-validator.mjs +52 -21
- package/esm/validators/base-validator.mjs.map +1 -1
- package/esm/validators/boolean-validator.d.mts +1 -3
- package/esm/validators/boolean-validator.d.mts.map +1 -1
- package/esm/validators/computed-validator.d.mts +3 -4
- package/esm/validators/computed-validator.d.mts.map +1 -1
- package/esm/validators/computed-validator.mjs +4 -4
- package/esm/validators/computed-validator.mjs.map +1 -1
- package/esm/validators/date-validator.d.mts +2 -3
- package/esm/validators/date-validator.d.mts.map +1 -1
- package/esm/validators/discriminated-union-validator.d.mts +3 -3
- package/esm/validators/discriminated-union-validator.d.mts.map +1 -1
- package/esm/validators/discriminated-union-validator.mjs +1 -1
- package/esm/validators/discriminated-union-validator.mjs.map +1 -1
- package/esm/validators/float-validator.d.mts +1 -3
- package/esm/validators/float-validator.d.mts.map +1 -1
- package/esm/validators/index.d.mts +2 -1
- package/esm/validators/index.mjs +1 -1
- package/esm/validators/instanceof-validator.d.mts +1 -3
- package/esm/validators/instanceof-validator.d.mts.map +1 -1
- package/esm/validators/int-validator.d.mts +1 -3
- package/esm/validators/int-validator.d.mts.map +1 -1
- package/esm/validators/lazy-validator.d.mts +2 -3
- package/esm/validators/lazy-validator.d.mts.map +1 -1
- package/esm/validators/lazy-validator.mjs +1 -1
- package/esm/validators/lazy-validator.mjs.map +1 -1
- package/esm/validators/literal-validator.d.mts +1 -3
- package/esm/validators/literal-validator.d.mts.map +1 -1
- package/esm/validators/managed-validator.d.mts +3 -4
- package/esm/validators/managed-validator.d.mts.map +1 -1
- package/esm/validators/methods/equality-conditional-methods.d.mts +3 -4
- package/esm/validators/methods/equality-conditional-methods.d.mts.map +1 -1
- package/esm/validators/methods/forbidden-methods.d.mts +2 -3
- package/esm/validators/methods/forbidden-methods.d.mts.map +1 -1
- package/esm/validators/methods/present-methods.d.mts +2 -3
- package/esm/validators/methods/present-methods.d.mts.map +1 -1
- package/esm/validators/methods/required-methods.d.mts +3 -4
- package/esm/validators/methods/required-methods.d.mts.map +1 -1
- package/esm/validators/number-validator.d.mts +1 -3
- package/esm/validators/number-validator.d.mts.map +1 -1
- package/esm/validators/number-validator.mjs +12 -8
- package/esm/validators/number-validator.mjs.map +1 -1
- package/esm/validators/numeric-validator.d.mts +1 -3
- package/esm/validators/numeric-validator.d.mts.map +1 -1
- package/esm/validators/object-validator.d.mts +2 -3
- package/esm/validators/object-validator.d.mts.map +1 -1
- package/esm/validators/object-validator.mjs +9 -7
- package/esm/validators/object-validator.mjs.map +1 -1
- package/esm/validators/primitive-validator.d.mts +1 -3
- package/esm/validators/primitive-validator.d.mts.map +1 -1
- package/esm/validators/record-validator.d.mts +2 -3
- package/esm/validators/record-validator.d.mts.map +1 -1
- package/esm/validators/record-validator.mjs +3 -3
- package/esm/validators/record-validator.mjs.map +1 -1
- package/esm/validators/scalar-validator.d.mts +1 -3
- package/esm/validators/scalar-validator.d.mts.map +1 -1
- package/esm/validators/scalar-validator.mjs.map +1 -1
- package/esm/validators/string-validator.d.mts +1 -3
- package/esm/validators/string-validator.d.mts.map +1 -1
- package/esm/validators/tuple-validator.d.mts +2 -3
- package/esm/validators/tuple-validator.d.mts.map +1 -1
- package/esm/validators/tuple-validator.mjs +3 -3
- package/esm/validators/tuple-validator.mjs.map +1 -1
- package/esm/validators/union-validator.d.mts +4 -4
- package/esm/validators/union-validator.d.mts.map +1 -1
- package/esm/validators/union-validator.mjs +3 -1
- package/esm/validators/union-validator.mjs.map +1 -1
- package/llms-full.txt +656 -527
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/skills/bridge-standard-schema/SKILL.md +60 -36
- package/skills/compose-seal-modifiers/SKILL.md +89 -53
- package/skills/define-structural-shape/SKILL.md +41 -27
- package/skills/define-structural-shape/array-methods.md +27 -27
- package/skills/define-structural-shape/object-methods.md +38 -30
- package/skills/extend-seal-with-plugins/SKILL.md +39 -5
- package/skills/generate-json-schema/SKILL.md +13 -13
- package/skills/handle-seal-errors/SKILL.md +23 -23
- package/skills/index.md +4 -1
- package/skills/overview/SKILL.md +10 -2
- package/skills/pick-seal-primitive/SKILL.md +45 -45
- package/skills/pick-seal-primitive/date-methods.md +96 -96
- package/skills/pick-seal-primitive/number-methods.md +55 -55
- package/skills/pick-seal-primitive/string-methods.md +107 -102
- package/skills/seal-basics/SKILL.md +14 -14
- package/cjs/object--LXRt5RF.cjs.map +0 -1
- package/cjs/required-methods-DYsRO9aZ.cjs.map +0 -1
- package/cjs/string-DsqnKoSB.cjs.map +0 -1
package/llms-full.txt
CHANGED
|
@@ -8,35 +8,36 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: bridge-standard-schema
|
|
11
|
-
description: 'Diagnose `StandardSchemaV1` interop —
|
|
11
|
+
description: 'Diagnose `StandardSchemaV1` interop — input/output class parameters and primitive factory intersections, why typed slots reject a schema, cascade `Model<TSchema>` variance. Triggers: `StandardSchemaV1`, `~standard`, `Infer`, `ObjectValidator`, `StringValidator`, `LiteralValidator`, `BaseValidator`, `Model<TSchema>`, `StandardJSONSchemaV1`, `Result<unknown>`; "StandardSchemaV1 slot rejecting my schema", "Result<unknown> error", "drop as unknown as cast", "cascade Model schema variance"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: foundations — the `seal-basics` topic; JSON Schema gen — the `generate-json-schema` topic; competing spec `@standard-schema/spec`.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# The Standard Schema bridge
|
|
15
15
|
|
|
16
16
|
Seal validators implement [Standard Schema V1](https://standardschema.dev) — every validator exposes a `~standard` member with `validate()`, JSON Schema metadata, and inferred types. Any consumer that accepts `StandardSchemaV1<T>` (`@warlock.js/ai` supervisor `output`, ai tool `input`, LangGraph state, OpenAI structured outputs, TanStack Form, Conform) accepts a seal schema directly.
|
|
17
17
|
|
|
18
|
-
This skill
|
|
18
|
+
This skill covers the input and output types exposed through `~standard`, and
|
|
19
|
+
compatibility with consumers such as Cascade model schemas and AI tool schemas.
|
|
19
20
|
|
|
20
21
|
## The shape of the bridge
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
`ObjectValidator<TSchema>` already threads `Infer.Input` and `Infer.Output` shapes
|
|
24
|
+
into `BaseValidator<TInput, TOutput>`. Both `v.object(fields)` and the standalone
|
|
25
|
+
`object(fields)` factory return that class directly. No extra Standard Schema
|
|
26
|
+
intersection is needed for objects: avoiding one keeps large composed schemas
|
|
27
|
+
cheaper for TypeScript to instantiate.
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
object: <T extends Schema>(schema: T) =>
|
|
27
|
-
new ObjectValidator<T>(schema) as ObjectValidator<T>
|
|
28
|
-
& StandardSchemaV1<Infer<ObjectValidator<T>>>;
|
|
29
|
-
|
|
30
|
-
string: () =>
|
|
31
|
-
new StringValidator() as StringValidator & StandardSchemaV1<string>;
|
|
29
|
+
Other factories add a Standard Schema intersection when their class has no precise
|
|
30
|
+
generic output declaration, for example:
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// ...one intersection per factory.
|
|
32
|
+
```ts
|
|
33
|
+
// Inside Seal's factory:
|
|
34
|
+
string: () => new StringValidator() as StringValidator & StandardSchemaV1<string>;
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
Keep input and output distinct when writing consumer types. Defaults and catches
|
|
38
|
+
can make input fields optional while output fields are required. Avoid computing
|
|
39
|
+
`Infer<this>` in the base class: object shapes are already supplied as generic
|
|
40
|
+
parameters, and recursively walking the full class increases compiler work.
|
|
40
41
|
|
|
41
42
|
## What this means for app code
|
|
42
43
|
|
|
@@ -58,7 +59,7 @@ ai.tool({
|
|
|
58
59
|
name: "create_user",
|
|
59
60
|
description: "Create a new user",
|
|
60
61
|
input: userSchema,
|
|
61
|
-
execute: async input => createUser(input),
|
|
62
|
+
execute: async (input) => createUser(input),
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
// ✅ supervisor output — no cast
|
|
@@ -82,7 +83,7 @@ const schema = v.object({
|
|
|
82
83
|
blocks: v.array(v.literal("text", "image")).optional(),
|
|
83
84
|
});
|
|
84
85
|
|
|
85
|
-
type Output = Infer<typeof schema>;
|
|
86
|
+
type Output = Infer.Output<typeof schema>;
|
|
86
87
|
// { email: string; blocks?: ("text" | "image")[] }
|
|
87
88
|
```
|
|
88
89
|
|
|
@@ -92,17 +93,12 @@ Hand-rolled parallel types drift the moment a field changes — and seal's tight
|
|
|
92
93
|
|
|
93
94
|
Three failure modes you'll see in real projects:
|
|
94
95
|
|
|
95
|
-
### 1.
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
// ❌ Discards the phantom intersection
|
|
99
|
-
const schema: ObjectValidator<{ email: StringValidator }> = v.object({...});
|
|
100
|
-
|
|
101
|
-
// ✅ Let inference run
|
|
102
|
-
const schema = v.object({...});
|
|
103
|
-
```
|
|
96
|
+
### 1. An annotation discarded schema details
|
|
104
97
|
|
|
105
|
-
|
|
98
|
+
Let the factory infer its concrete schema. An annotation such as bare
|
|
99
|
+
`ObjectValidator` loses the field types; primitive factories also lose their
|
|
100
|
+
Standard Schema intersection when annotated with a bare primitive class.
|
|
101
|
+
`ObjectValidator<typeof fields>` preserves an object's input and output types.
|
|
106
102
|
|
|
107
103
|
### 2. The schema's inferred shape doesn't match the target type
|
|
108
104
|
|
|
@@ -117,23 +113,51 @@ The supervisor's explicit `<MyOutput>` generic is a constraint the schema must s
|
|
|
117
113
|
- **Drop the explicit generic** — let the supervisor infer output type from the schema. The hand-rolled type was probably documentation-only anyway.
|
|
118
114
|
- **Align them** — if the hand-rolled type is the source of truth (e.g. a domain type from elsewhere), make the schema match.
|
|
119
115
|
|
|
120
|
-
### 3. The error says `Result<unknown>`
|
|
116
|
+
### 3. The error says `Result<unknown>`
|
|
121
117
|
|
|
122
|
-
|
|
118
|
+
Check whether an annotation erased a factory's output type, and compare
|
|
119
|
+
`Infer.Input<typeof schema>` and `Infer.Output<typeof schema>` with the consumer's
|
|
120
|
+
expected types. Output slots should use the post-default output shape. Objects
|
|
121
|
+
carry that shape directly through their class generics; primitive factories add
|
|
122
|
+
it through their Standard Schema intersection.
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
## Union and record outputs
|
|
125
|
+
|
|
126
|
+
Use `Infer.Output<T>` or `StandardSchemaV1.InferOutput<T>` for downstream data.
|
|
127
|
+
Unions and records preserve their children's output types independently of input
|
|
128
|
+
shapes. Defaults and catches make object fields required in output. Their factory
|
|
129
|
+
intersections put the precise Standard Schema signature first, so direct
|
|
130
|
+
`~standard.validate()` calls return the declared output. `Infer.Output` reads the
|
|
131
|
+
output metadata rather than inferring from overloaded validation signatures. A union
|
|
132
|
+
returns the selected branch's validated data before its own transformers and
|
|
133
|
+
refinements; it does not discard branch defaults, computed values or transformations.
|
|
125
134
|
|
|
126
135
|
```ts
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
136
|
+
const entry = v.object({ count: v.int().optional().default(1) });
|
|
137
|
+
const entries = v.record(entry);
|
|
138
|
+
type Entries = Infer.Output<typeof entries>;
|
|
139
|
+
// Record<string, { count: number }>
|
|
140
|
+
const maybeEntry = v.union([entry, v.string()]);
|
|
141
|
+
type MaybeEntry = Infer.Output<typeof maybeEntry>;
|
|
142
|
+
// { count: number } | string
|
|
130
143
|
```
|
|
131
144
|
|
|
132
|
-
|
|
145
|
+
When upgrading, union output can change through member trimming, Date parsing,
|
|
146
|
+
defaults, computed fields and unknown-key stripping. An explicit `undefined`
|
|
147
|
+
member output drops its containing object field. Keep the raw input before
|
|
148
|
+
validation if storage, comparisons or serialization also need that value.
|
|
149
|
+
|
|
150
|
+
## Compiler budget
|
|
151
|
+
|
|
152
|
+
From `packages/seal`, run `pnpm bench:types --check` after changes to inference or
|
|
153
|
+
factory types. CI runs the same deterministic composed-schema fixture when Seal
|
|
154
|
+
is selected. Instantiations and type counts may grow at most 5% above the checked-in
|
|
155
|
+
baseline. Refresh with `node scripts/bench-types.mjs --update` only after reviewing
|
|
156
|
+
the measurements; time and memory are diagnostic rather than gated.
|
|
133
157
|
|
|
134
158
|
## Why not `Omit<class, "~standard"> & StandardSchemaV1<T>`?
|
|
135
159
|
|
|
136
|
-
Tempting — it would force the factory's `~standard` to fully replace the class's wider one, fixing the misleading `Result<unknown>` error message. **Don't do it.** `Omit` on a class instance type triggers the same variance trap as putting `Infer<this>` on the class itself: cascade explodes with hundreds of `ObjectValidator<{specific}>` vs `ObjectValidator<TSchema>` errors.
|
|
160
|
+
Tempting — it would force the factory's `~standard` to fully replace the class's wider one, fixing the misleading `Result<unknown>` error message. **Don't do it.** `Omit` on a class instance type triggers the same variance trap as putting `Infer<this>` on the class itself: cascade explodes with hundreds of `ObjectValidator<{specific}>` vs `ObjectValidator<TSchema>` errors. Keep object factories as their existing generic class, and retain factory-side intersections for primitive classes that need them. Both preserve the validator's class members for generic consumers.
|
|
137
161
|
|
|
138
162
|
## Why not import `@standard-schema/spec` instead of forking the types?
|
|
139
163
|
|
|
@@ -167,31 +191,61 @@ When `validate()` runs against a value, the order is:
|
|
|
167
191
|
3. optional/required check — decide whether to run rules
|
|
168
192
|
4. requiredRule — required-condition rule (.requiredIf, .requiredWith, etc.)
|
|
169
193
|
5. rules — every other rule, in declaration order
|
|
170
|
-
6.
|
|
194
|
+
6. children — validate container members, fill defaults and computed fields
|
|
195
|
+
7. transformers — reshape value into data (.toISOString, .toLowerCase)
|
|
196
|
+
8. refinements — typed custom checks against successful output
|
|
171
197
|
```
|
|
172
198
|
|
|
173
199
|
If a rule fails, transformers don't run for that field.
|
|
174
200
|
|
|
175
201
|
**Mutator vs transformer mental model:**
|
|
202
|
+
|
|
176
203
|
- **Mutator** = pre-validation reshape. `v.date()` ships one that parses strings. Use when you want rules to see the reshaped value.
|
|
177
204
|
- **Transformer** = post-validation reshape. Lands in `data`. Use when you only care about the output form.
|
|
178
205
|
|
|
179
206
|
```ts
|
|
180
207
|
// trim BEFORE length check — use a mutator
|
|
181
|
-
v.string()
|
|
208
|
+
v.string()
|
|
209
|
+
.addMutator((s) => s.trim())
|
|
210
|
+
.min(3);
|
|
182
211
|
// " Hi " → mutator trims → "Hi" → fails min(3)
|
|
183
212
|
|
|
184
213
|
// trim only the OUTPUT — use a transformer (.trim() on string is a transformer)
|
|
185
|
-
v.string().min(3).trim()
|
|
214
|
+
v.string().min(3).trim();
|
|
186
215
|
// " Hi " → rules see " Hi " (length 6, passes min(3)) → trim → data = "Hi"
|
|
187
216
|
```
|
|
188
217
|
|
|
218
|
+
## `.refine()`
|
|
219
|
+
|
|
220
|
+
Refinements receive the validator's output type, after children, defaults, computed
|
|
221
|
+
fields and output transformers. They skip failed validation and absent/null values,
|
|
222
|
+
including when `firstErrorOnly: false`. Return an error string to reject the value;
|
|
223
|
+
return `undefined` to accept it. Async callbacks are supported. Use `.addRule()`
|
|
224
|
+
when a check must run against input before children validate.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
v.object({ count: v.int().optional().default(1) }).refine((value) => {
|
|
228
|
+
// count is a number, and defaults have already been applied.
|
|
229
|
+
return value.count > 10 ? "Too many items" : undefined;
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Successful empty defaults such as `.default("")` are refined too. `.merge()`
|
|
234
|
+
appends the other object's refinements; `.extend()` imports only fields. Cloning
|
|
235
|
+
and field-selection methods preserve the receiver's refinements, so callbacks
|
|
236
|
+
must tolerate fields removed by `.pick()` or `.without()`.
|
|
237
|
+
|
|
238
|
+
Output transformers do not yet change declared output types: a
|
|
239
|
+
`v.date().toISOString()` refinement receives a string at runtime despite the
|
|
240
|
+
`Date` declaration. Use a Date input rule, or convert after validation; moving
|
|
241
|
+
`.refine()` earlier in the chain does not move it before transformers.
|
|
242
|
+
|
|
189
243
|
## `.required()` / `.optional()` / `.present()`
|
|
190
244
|
|
|
191
245
|
```ts
|
|
192
|
-
v.string()
|
|
193
|
-
v.string().optional()
|
|
194
|
-
v.string().present()
|
|
246
|
+
v.string(); // already required inside v.object
|
|
247
|
+
v.string().optional(); // type: string | undefined — { isOptional: true } brand
|
|
248
|
+
v.string().present(); // must exist, may be "" / null
|
|
195
249
|
```
|
|
196
250
|
|
|
197
251
|
**Required is the default inside `v.object`.** Most schemas read cleaner without `.required()` — `Infer<>` already shows what's required (no `?`) vs optional (`?`). Calling `.required()` explicitly is harmless and accepted, but the canonical seal style is to skip it. Keep `.optional()` explicit (it changes behavior); skip `.required()` (it doesn't).
|
|
@@ -199,9 +253,11 @@ v.string().present() // must exist, may be "" / null
|
|
|
199
253
|
The `{ isOptional: true }` brand survives chaining (`.optional().min(3)` is still optional) and `Infer<>` reads it to make the key optional.
|
|
200
254
|
|
|
201
255
|
**When `.required()` is still useful:**
|
|
202
|
-
|
|
256
|
+
|
|
257
|
+
- It's not needed for conditional rules — `.requiredIf(field, value)` and friends _replace_ the default required-condition slot, so they work standalone.
|
|
203
258
|
|
|
204
259
|
Conditional variants (run inside `v.object` only):
|
|
260
|
+
|
|
205
261
|
- `.requiredIf(field, value)` / `.requiredIfSibling(field, value)`
|
|
206
262
|
- `.requiredWith(field)` / `.requiredWithSibling(field)`
|
|
207
263
|
- `.requiredWithout(field)` / `.requiredWithoutSibling(field)`
|
|
@@ -213,22 +269,22 @@ Conditional variants (run inside `v.object` only):
|
|
|
213
269
|
## `.nullable()` / `.notNullable()` / `.nullish()`
|
|
214
270
|
|
|
215
271
|
```ts
|
|
216
|
-
v.string().nullable()
|
|
217
|
-
v.string().nullish()
|
|
272
|
+
v.string().nullable(); // type: string | null
|
|
273
|
+
v.string().nullish(); // sugar for .optional().nullable()
|
|
218
274
|
```
|
|
219
275
|
|
|
220
|
-
Independent of optional — a field can be required
|
|
276
|
+
Independent of optional — a field can be required _and_ nullable. Defaults to non-nullable. The `{ isNullable: true }` brand widens both `Infer.Input` and `Infer.Output` with `| null`.
|
|
221
277
|
|
|
222
278
|
## `.default(value | callback)`
|
|
223
279
|
|
|
224
280
|
```ts
|
|
225
|
-
v.string().default("guest")
|
|
226
|
-
v.int().default(0)
|
|
227
|
-
v.date().default(() => new Date())
|
|
228
|
-
v.array(v.string()).default([])
|
|
281
|
+
v.string().default("guest");
|
|
282
|
+
v.int().default(0);
|
|
283
|
+
v.date().default(() => new Date()); // lazy — fresh each validation
|
|
284
|
+
v.array(v.string()).default([]);
|
|
229
285
|
```
|
|
230
286
|
|
|
231
|
-
If input is `undefined` (or missing), the default is used and rules run against it. Pass a callback for fresh-per-run values. The default fires
|
|
287
|
+
If input is `undefined` (or missing), the default is used and rules run against it. Pass a callback for fresh-per-run values. The default fires _before_ rules — so `v.string().min(3).default("a")` fails because `"a"` is shorter than 3.
|
|
232
288
|
|
|
233
289
|
Date sugar: `v.date().defaultNow()` ≡ `.default(() => new Date())`.
|
|
234
290
|
|
|
@@ -236,7 +292,7 @@ The `{ hasDefault: true }` brand makes the key optional in `Infer.Input` (caller
|
|
|
236
292
|
|
|
237
293
|
## `.catch(fallback)`
|
|
238
294
|
|
|
239
|
-
Rescues
|
|
295
|
+
Rescues _failed_ validation by substituting a fallback. Complement to `.default()`:
|
|
240
296
|
|
|
241
297
|
- `.default(x)` fires when input is **absent**
|
|
242
298
|
- `.catch(y)` fires when input is **present but invalid**
|
|
@@ -271,7 +327,7 @@ const schema = v.object({
|
|
|
271
327
|
retries: v.int().catch(3),
|
|
272
328
|
});
|
|
273
329
|
|
|
274
|
-
type In
|
|
330
|
+
type In = Infer.Input<typeof schema>;
|
|
275
331
|
// {
|
|
276
332
|
// bio?: string;
|
|
277
333
|
// status?: Status; ← default makes caller optional
|
|
@@ -285,10 +341,11 @@ type Out = Infer.Output<typeof schema>;
|
|
|
285
341
|
// retries: number; ← catch guarantees a value
|
|
286
342
|
// }
|
|
287
343
|
|
|
288
|
-
type Default = Infer<typeof schema>;
|
|
344
|
+
type Default = Infer<typeof schema>; // alias for Infer.Input
|
|
289
345
|
```
|
|
290
346
|
|
|
291
347
|
**When to reach for which:**
|
|
348
|
+
|
|
292
349
|
- `Infer.Input<T>` (or bare `Infer<T>`) — for HTTP request bodies, form payloads, DTOs, anything pre-validation. **The common case in HTTP-shaped code.**
|
|
293
350
|
- `Infer.Output<T>` — for Cascade `Model<>` params, validated state, anywhere downstream of `validate()`.
|
|
294
351
|
|
|
@@ -298,26 +355,26 @@ Both widen with `| null` when `.nullable()` is set.
|
|
|
298
355
|
|
|
299
356
|
Three failure modes, three different rescue mechanisms:
|
|
300
357
|
|
|
301
|
-
| Input state
|
|
302
|
-
|
|
|
303
|
-
| Field absent
|
|
304
|
-
| Field absent (no default)
|
|
305
|
-
| Field present and invalid
|
|
306
|
-
| Field is `null`
|
|
307
|
-
| Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is
|
|
358
|
+
| Input state | Rescued by | What appears in `data` |
|
|
359
|
+
| --------------------------------------- | -------------------------------------- | ---------------------- |
|
|
360
|
+
| Field absent | `.default(x)` | `x` |
|
|
361
|
+
| Field absent (no default) | `.optional()` | Key omitted entirely |
|
|
362
|
+
| Field present and invalid | `.catch(y)` | `y` |
|
|
363
|
+
| Field is `null` | `.nullable()` | `null` |
|
|
364
|
+
| Field present, empty (`""`, `[]`, `{}`) | (none needed — empty is a valid value) | Preserved as-is |
|
|
308
365
|
|
|
309
366
|
Full truth table for an `.optional()` field inside `v.object(…)`:
|
|
310
367
|
|
|
311
|
-
| Input
|
|
312
|
-
|
|
|
313
|
-
| Field absent
|
|
314
|
-
| Field is explicit `undefined`
|
|
315
|
-
| Field is `null` (no `.nullable()`)
|
|
316
|
-
| Field is `null` with `.nullable()`
|
|
317
|
-
| Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent
|
|
318
|
-
| Field is `.default(x)` and absent
|
|
319
|
-
| Field is `.default(x)` and present
|
|
320
|
-
| Field is `.catch(y)` and validation fails
|
|
368
|
+
| Input | What appears in `data` |
|
|
369
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
370
|
+
| Field absent | Key **omitted entirely** (not `undefined`-valued) |
|
|
371
|
+
| Field is explicit `undefined` | Key omitted (treated identically to absent) |
|
|
372
|
+
| Field is `null` (no `.nullable()`) | Key omitted — for an **optional** field, `null` coalesces to empty and the (cleared) required rule doesn't fire. On a **required** field the same `null` triggers a validation error. |
|
|
373
|
+
| Field is `null` with `.nullable()` | Key is `null` |
|
|
374
|
+
| Field present and empty (`""`, `[]`, `{}`) | Preserved as-is — empty ≠ absent |
|
|
375
|
+
| Field is `.default(x)` and absent | Key is `x` (default fires, then rules run on `x`) |
|
|
376
|
+
| Field is `.default(x)` and present | Caller value wins; default is unused |
|
|
377
|
+
| Field is `.catch(y)` and validation fails | Key is `y` (catch rescues) |
|
|
321
378
|
|
|
322
379
|
```ts
|
|
323
380
|
const schema = v.object({
|
|
@@ -325,11 +382,12 @@ const schema = v.object({
|
|
|
325
382
|
embedding: v.array(v.number()).optional(),
|
|
326
383
|
});
|
|
327
384
|
|
|
328
|
-
(await validate(schema, {}))
|
|
329
|
-
|
|
330
|
-
//
|
|
385
|
+
(await validate(schema, {}))
|
|
386
|
+
.data
|
|
387
|
+
// → {} — neither key appears
|
|
388
|
+
// (NOT { metadata: {}, embedding: [] })
|
|
331
389
|
|
|
332
|
-
(await validate(schema, { metadata: {}, embedding: [] })).data
|
|
390
|
+
(await validate(schema, { metadata: {}, embedding: [] })).data;
|
|
333
391
|
// → { metadata: {}, embedding: [] } — present-empty is preserved
|
|
334
392
|
```
|
|
335
393
|
|
|
@@ -342,12 +400,12 @@ Collection validators (`v.record`, `v.array`, `v.tuple`) explicitly honor this
|
|
|
342
400
|
Available on `v.string()`, `v.number()`, `v.int()`, `v.float()`, `v.boolean()`, `v.scalar()`:
|
|
343
401
|
|
|
344
402
|
```ts
|
|
345
|
-
v.string().in(["admin", "user", "guest"])
|
|
346
|
-
v.string().oneOf(["a", "b"])
|
|
347
|
-
v.string().notIn(["banned", "blocked"])
|
|
348
|
-
v.string().forbids(["banned"])
|
|
349
|
-
v.number().allowsOnly([1, 2, 3])
|
|
350
|
-
v.string().enum(MyTSEnum)
|
|
403
|
+
v.string().in(["admin", "user", "guest"]); // value must match one
|
|
404
|
+
v.string().oneOf(["a", "b"]); // alias for .in
|
|
405
|
+
v.string().notIn(["banned", "blocked"]); // value must NOT match
|
|
406
|
+
v.string().forbids(["banned"]); // alias for .notIn
|
|
407
|
+
v.number().allowsOnly([1, 2, 3]); // stricter — explicit allowlist
|
|
408
|
+
v.string().enum(MyTSEnum); // accepts a TS enum object via Object.values
|
|
351
409
|
```
|
|
352
410
|
|
|
353
411
|
For literal-typed narrowing (`"admin" | "user" | "guest"` instead of `string`), use `v.literal(...)` instead — `oneOf` keeps the broader primitive type.
|
|
@@ -359,10 +417,10 @@ v.object({
|
|
|
359
417
|
email: v.string(),
|
|
360
418
|
password: v.string(),
|
|
361
419
|
passwordConfirm: v.string().sameAs("password").omit(),
|
|
362
|
-
})
|
|
420
|
+
});
|
|
363
421
|
```
|
|
364
422
|
|
|
365
|
-
`.omit()` keeps the field in
|
|
423
|
+
`.omit()` keeps the field in _validation_ but drops it from `data` and from `Infer<>`. Use for confirmation/checksum fields that exist only for cross-field rules. `.exclude()` is the same idea, used internally for managed/computed plumbing.
|
|
366
424
|
|
|
367
425
|
## `.label("Display Name")` — field display name
|
|
368
426
|
|
|
@@ -371,7 +429,7 @@ To control the `:input` placeholder in a field's own messages, call `.label()` o
|
|
|
371
429
|
```ts
|
|
372
430
|
v.object({
|
|
373
431
|
email_address: v.string().label("Email Address"),
|
|
374
|
-
})
|
|
432
|
+
});
|
|
375
433
|
// Error message: "The Email Address is required" (instead of "The email_address is required")
|
|
376
434
|
```
|
|
377
435
|
|
|
@@ -379,12 +437,14 @@ v.object({
|
|
|
379
437
|
|
|
380
438
|
### `.attributes({ ... })` is a different tool
|
|
381
439
|
|
|
382
|
-
`.attributes()` does NOT relabel a field's own `:input`. It supplies named substitution values consumed by the translation layer and by rules that reference
|
|
440
|
+
`.attributes()` does NOT relabel a field's own `:input`. It supplies named substitution values consumed by the translation layer and by rules that reference _other_ fields (e.g. `matches`):
|
|
383
441
|
|
|
384
442
|
```ts
|
|
385
|
-
v.string()
|
|
386
|
-
|
|
387
|
-
|
|
443
|
+
v.string()
|
|
444
|
+
.sameAs("confirmPassword")
|
|
445
|
+
.attributes({
|
|
446
|
+
matches: { confirmPassword: "Confirm Password" },
|
|
447
|
+
});
|
|
388
448
|
```
|
|
389
449
|
|
|
390
450
|
For per-field display names use `.label()`; for translated messages wire the `translateRule` / `translateAttribute` hooks via `configureSeal()`.
|
|
@@ -412,7 +472,7 @@ Switch back with `.immutable`. Default is fine 99% of the time. Reach for `.muta
|
|
|
412
472
|
|
|
413
473
|
## Things NOT to do
|
|
414
474
|
|
|
415
|
-
- Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming
|
|
475
|
+
- Don't put a transformer where a mutator belongs — `.trim()` is a transformer; if you need trimming _before_ `.min()`, use `.addMutator(s => s.trim())`.
|
|
416
476
|
- Don't combine `.required()` and `.optional()` on the same chain — last wins, but the intent is unclear; pick one.
|
|
417
477
|
- Don't chain `.default("a")` with `.min(3)` and expect `"a"` to pass — the default goes through rules.
|
|
418
478
|
- Don't expect `.requiredIf()` to work on a standalone validator outside `v.object` — sibling resolution silently passes.
|
|
@@ -437,20 +497,20 @@ v.object({
|
|
|
437
497
|
email: v.string().email(),
|
|
438
498
|
age: v.int().min(13).optional(),
|
|
439
499
|
role: v.literal("admin", "user", "guest"),
|
|
440
|
-
})
|
|
500
|
+
});
|
|
441
501
|
```
|
|
442
502
|
|
|
443
503
|
- **Required by default.** `.optional()` to opt out — the `{ isOptional: true }` brand makes the key optional in `Infer<>`.
|
|
444
504
|
- **Unknown keys policy.** By default extra keys are silently dropped from `data`. Toggle with `.allowUnknown()` (forward as-is), `.stripUnknown()` (explicit drop), or `.allow(...keys)` (whitelist specific extras). See [`object-methods.md`](./object-methods.md).
|
|
445
505
|
- **Schema composition.** `.extend(schema)`, `.merge(other)`, `.pick(...keys)`, `.without(...keys)`, `.partial(...keys)`, `.requiredFields(...keys)` — all in [`object-methods.md`](./object-methods.md).
|
|
446
|
-
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields
|
|
506
|
+
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`) attach to fields _inside_ a `v.object`. Without a parent, sibling resolution silently passes.
|
|
447
507
|
|
|
448
508
|
## `v.array` — homogeneous lists
|
|
449
509
|
|
|
450
510
|
```ts
|
|
451
|
-
v.array(v.string())
|
|
452
|
-
v.array(userSchema)
|
|
453
|
-
v.array(v.array(v.int()))
|
|
511
|
+
v.array(v.string()); // type: string[]
|
|
512
|
+
v.array(userSchema); // type: User[]
|
|
513
|
+
v.array(v.array(v.int())); // type: number[][] — recursive inference
|
|
454
514
|
```
|
|
455
515
|
|
|
456
516
|
The inner validator runs against each element; failure on any element fails the array. Method surface (`.min`, `.max`, `.unique`, `.sorted`, `.flip`, …) in [`array-methods.md`](./array-methods.md).
|
|
@@ -458,9 +518,9 @@ The inner validator runs against each element; failure on any element fails the
|
|
|
458
518
|
## `v.record` — homogeneous values, dynamic keys
|
|
459
519
|
|
|
460
520
|
```ts
|
|
461
|
-
v.record(v.int())
|
|
462
|
-
v.record(v.object({ count: v.int() }))
|
|
463
|
-
v.record()
|
|
521
|
+
v.record(v.int()); // type: Record<string, number>
|
|
522
|
+
v.record(v.object({ count: v.int() })); // type: Record<string, { count: number }>
|
|
523
|
+
v.record(); // type: Record<string, any>
|
|
464
524
|
```
|
|
465
525
|
|
|
466
526
|
Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but values share a schema. If keys are also constrained (e.g. only `"draft" | "published"`), use `v.object` with literal keys instead — the constraint lives in the type.
|
|
@@ -468,26 +528,40 @@ Reach for `v.record` when keys are dynamic (user-provided, dictionary-style) but
|
|
|
468
528
|
## `v.tuple` — positional types
|
|
469
529
|
|
|
470
530
|
```ts
|
|
471
|
-
v.tuple([v.string(), v.int(), v.boolean()])
|
|
472
|
-
v.tuple([v.literal("ok"), v.string()])
|
|
531
|
+
v.tuple([v.string(), v.int(), v.boolean()]); // type: [string, number, boolean]
|
|
532
|
+
v.tuple([v.literal("ok"), v.string()]); // type: ["ok", string]
|
|
473
533
|
```
|
|
474
534
|
|
|
475
535
|
Each position has its own validator; the array length must match the tuple length. Pair with `v.literal` at position 0 for result-tuple patterns (`["ok", data]` vs `["error", message]`).
|
|
476
536
|
|
|
537
|
+
Pass the validators as a literal list so each position keeps its own type, for input and validated output alike. A list typed `BaseValidator[]` (built at runtime) has no fixed positions, so its output is an array rather than a tuple.
|
|
538
|
+
|
|
477
539
|
## `v.union` — one of N validators (untagged)
|
|
478
540
|
|
|
479
541
|
```ts
|
|
480
|
-
v.union([v.string(), v.int()])
|
|
542
|
+
v.union([v.string(), v.int()]); // type: string | number
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Every branch whose type matches the value (`matchesType()`) is tried, in order, and the first that passes wins; the union returns that branch's validated output. `firstErrorOnly` limits the errors reported, never the branches tried, so a union of several object shapes accepts a value matching any of them:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
const owner = v.object({ kind: v.literal("owner"), id: v.literal("__owner__") });
|
|
549
|
+
const member = v.object({ kind: v.literal("member"), id: v.string() });
|
|
550
|
+
|
|
551
|
+
await validate(v.union([owner, member]), { kind: "member", id: "x" });
|
|
552
|
+
// → valid, data: { kind: "member", id: "x" }
|
|
481
553
|
```
|
|
482
554
|
|
|
483
|
-
|
|
555
|
+
When no branch passes, the union reports the **closest** failed branch, picked by: fewest mismatched `v.literal(...)` fields, then fewest errors, then the deepest error path, then declaration order. Its errors keep their own paths (`actor.id`, not `actor`) and have the type `"union"` — the first one by default, all of them with `firstErrorOnly: false`. A custom `errorMessage` (`v.union([...], "Unknown actor")`), or a value no branch can hold by type, gives a single error at the union's path.
|
|
556
|
+
|
|
557
|
+
For object shapes that share a tag field, `v.discriminatedUnion` is still the better fit: it routes straight to one branch instead of trying each, and narrows exactly in TypeScript.
|
|
484
558
|
|
|
485
559
|
## `v.discriminatedUnion` — tagged unions (recommended for objects)
|
|
486
560
|
|
|
487
561
|
```ts
|
|
488
562
|
const email = v.object({ type: v.literal("email"), email: v.string().email() });
|
|
489
|
-
const sms
|
|
490
|
-
const push
|
|
563
|
+
const sms = v.object({ type: v.literal("sms"), phone: v.string() });
|
|
564
|
+
const push = v.object({ type: v.literal("push"), deviceId: v.string() });
|
|
491
565
|
|
|
492
566
|
const notif = v.discriminatedUnion("type", [email, sms, push]);
|
|
493
567
|
|
|
@@ -499,8 +573,8 @@ type Notif = Infer<typeof notif>;
|
|
|
499
573
|
|
|
500
574
|
Routes payloads by reading the discriminator field (here `type`), looking it up in a key→branch map built at construction time, and delegating to the matching branch only. Benefits over plain `v.union`:
|
|
501
575
|
|
|
502
|
-
- **Precise errors.** Failures come from the
|
|
503
|
-
- **O(1) routing** instead of
|
|
576
|
+
- **Precise errors.** Failures come from the branch the tag names, not from the closest guess.
|
|
577
|
+
- **O(1) routing** instead of trying each branch.
|
|
504
578
|
- **Exact TS inference** — discriminated union with narrowing inside `if (x.type === "email")` blocks.
|
|
505
579
|
- **Cleaner JSON Schema** — `oneOf` with literal discriminators; OpenAI-strict accepts it.
|
|
506
580
|
|
|
@@ -538,20 +612,20 @@ Use for: trees (categories, file systems), threaded data (comment chains), mutua
|
|
|
538
612
|
|
|
539
613
|
## Quick map
|
|
540
614
|
|
|
541
|
-
| Want
|
|
542
|
-
|
|
|
543
|
-
| Fixed-shape record
|
|
544
|
-
| Dynamic keys, same value shape
|
|
545
|
-
| List of items
|
|
546
|
-
| Position-typed array
|
|
547
|
-
| One of N scalar types
|
|
548
|
-
| One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])`
|
|
549
|
-
| Self-referencing or forward reference
|
|
550
|
-
| One of N constants
|
|
615
|
+
| Want | Reach for |
|
|
616
|
+
| --------------------------------------- | ----------------------------------------------------------------------------- |
|
|
617
|
+
| Fixed-shape record | `v.object({...})` |
|
|
618
|
+
| Dynamic keys, same value shape | `v.record(valueSchema)` |
|
|
619
|
+
| List of items | `v.array(itemSchema)` |
|
|
620
|
+
| Position-typed array | `v.tuple([a, b, c])` |
|
|
621
|
+
| One of N scalar types | `v.union([...])` |
|
|
622
|
+
| One of N object shapes with a tag field | `v.discriminatedUnion(key, [...])` |
|
|
623
|
+
| Self-referencing or forward reference | `v.lazy(() => schema)` |
|
|
624
|
+
| One of N constants | `v.literal(...values)` (not structural — see the `pick-seal-primitive` topic) |
|
|
551
625
|
|
|
552
626
|
## A note on cascade Models
|
|
553
627
|
|
|
554
|
-
`@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains
|
|
628
|
+
`@warlock.js/cascade` Models declare `static schema: ObjectValidator<TSchema>`. Passing a `v.object({...})` works directly — the factory return widens to fit the Model's invariant generic without breaking type checking. If TS complains _"`ObjectValidator<{specific}>` is not assignable to `ObjectValidator<TSchema>`"_, that's the variance trap and the answer isn't to widen the schema — it's almost always that `Model<TSchema>` was parameterized with a hand-rolled type that drifted from the schema's inferred shape. Fix the type, not the schema. See the `bridge-standard-schema` topic.
|
|
555
629
|
|
|
556
630
|
## Method-surface reference
|
|
557
631
|
|
|
@@ -569,41 +643,41 @@ The inner validator runs against each element. Failure on any element fails the
|
|
|
569
643
|
|
|
570
644
|
## Length
|
|
571
645
|
|
|
572
|
-
| Method
|
|
573
|
-
|
|
574
|
-
| `.min(n, msg?)`
|
|
575
|
-
| `.minLength(n, msg?)`
|
|
576
|
-
| `.maxLength(n, msg?)`
|
|
577
|
-
| `.length(n, msg?)`
|
|
578
|
-
| `.between(a, b, msg?)`
|
|
579
|
-
| `.lengthBetween(a, b, msg?)` | alias for `.between()` | `minItems: a, maxItems: b` | —
|
|
646
|
+
| Method | Args | JSON Schema | Example |
|
|
647
|
+
| ---------------------------- | ---------------------- | -------------------------- | ------------------------------------ |
|
|
648
|
+
| `.min(n, msg?)` | n/a — see note below | — | use `.minLength(n)` |
|
|
649
|
+
| `.minLength(n, msg?)` | inclusive lower bound | `minItems: n` | `v.array(v.string()).minLength(1)` |
|
|
650
|
+
| `.maxLength(n, msg?)` | inclusive upper bound | `maxItems: n` | `v.array(v.string()).maxLength(10)` |
|
|
651
|
+
| `.length(n, msg?)` | exact length | `minItems=maxItems=n` | `v.array(v.string()).length(3)` |
|
|
652
|
+
| `.between(a, b, msg?)` | inclusive range | `minItems: a, maxItems: b` | `v.array(v.string()).between(1, 10)` |
|
|
653
|
+
| `.lengthBetween(a, b, msg?)` | alias for `.between()` | `minItems: a, maxItems: b` | — |
|
|
580
654
|
|
|
581
655
|
**Note on `.min`/`.max`:** the array validator does not expose `.min`/`.max` directly — use `.minLength`/`.maxLength` (or `.between`/`.length`). This is intentional: `.min`/`.max` would conflict with primitive value-comparison semantics if they ever bled into here.
|
|
582
656
|
|
|
583
657
|
## Uniqueness & sort
|
|
584
658
|
|
|
585
|
-
| Method
|
|
586
|
-
|
|
587
|
-
| `.unique(msg?)`
|
|
659
|
+
| Method | Args | Effect |
|
|
660
|
+
| --------------------------- | ----------------------------- | ----------------------------- |
|
|
661
|
+
| `.unique(msg?)` | — | every element is distinct |
|
|
588
662
|
| `.sorted(direction?, msg?)` | `"asc"` (default) or `"desc"` | array is monotonically sorted |
|
|
589
663
|
|
|
590
664
|
## Mutators (pre-validation reshape)
|
|
591
665
|
|
|
592
|
-
| Method
|
|
593
|
-
|
|
594
|
-
| `.flip()`
|
|
595
|
-
| `.reverse()`
|
|
596
|
-
| `.onlyUnique()`
|
|
597
|
-
| `.sort(direction?, key?)` | `"asc"` (default) or `"desc"`; optional sort key for object items | sort before validation
|
|
666
|
+
| Method | Args | Effect |
|
|
667
|
+
| ------------------------- | ----------------------------------------------------------------- | ------------------------ |
|
|
668
|
+
| `.flip()` | — | reverse the array |
|
|
669
|
+
| `.reverse()` | — | alias for `.flip()` |
|
|
670
|
+
| `.onlyUnique()` | — | dedupe before validation |
|
|
671
|
+
| `.sort(direction?, key?)` | `"asc"` (default) or `"desc"`; optional sort key for object items | sort before validation |
|
|
598
672
|
|
|
599
673
|
`onlyUnique()` and `unique()` differ: the mutator silently dedupes; the rule fails when duplicates are present. Pick by intent — coerce vs reject.
|
|
600
674
|
|
|
601
675
|
## Inner validator notes
|
|
602
676
|
|
|
603
677
|
```ts
|
|
604
|
-
v.array(v.string().email())
|
|
605
|
-
v.array(userSchema)
|
|
606
|
-
v.array(v.array(v.int()))
|
|
678
|
+
v.array(v.string().email()); // every element must be a valid email
|
|
679
|
+
v.array(userSchema); // every element must satisfy userSchema
|
|
680
|
+
v.array(v.array(v.int())); // matrix-ish — Infer is recursive
|
|
607
681
|
```
|
|
608
682
|
|
|
609
683
|
Element validators run with element-level context. `path` becomes `${parent}.${index}` for error messages. Cross-field rules inside element schemas resolve to that element's fields, not the array.
|
|
@@ -621,25 +695,25 @@ Element validators run with element-level context. `path` becomes `${parent}.${i
|
|
|
621
695
|
|
|
622
696
|
```ts
|
|
623
697
|
// Tags
|
|
624
|
-
v.array(v.string().min(1)).unique().between(1, 10)
|
|
698
|
+
v.array(v.string().min(1)).unique().between(1, 10);
|
|
625
699
|
|
|
626
700
|
// User list
|
|
627
|
-
v.array(userSchema).minLength(1)
|
|
701
|
+
v.array(userSchema).minLength(1);
|
|
628
702
|
|
|
629
703
|
// Pre-sorted, deduped
|
|
630
|
-
v.array(v.string()).onlyUnique().sort("asc")
|
|
704
|
+
v.array(v.string()).onlyUnique().sort("asc");
|
|
631
705
|
|
|
632
706
|
// Reverse-emit
|
|
633
|
-
v.array(v.string()).flip()
|
|
707
|
+
v.array(v.string()).flip();
|
|
634
708
|
|
|
635
709
|
// Matrix
|
|
636
|
-
v.array(v.array(v.number())).minLength(1)
|
|
710
|
+
v.array(v.array(v.number())).minLength(1);
|
|
637
711
|
|
|
638
712
|
// Sorted ids
|
|
639
|
-
v.array(v.string().uuid()).sorted("asc").unique()
|
|
713
|
+
v.array(v.string().uuid()).sorted("asc").unique();
|
|
640
714
|
|
|
641
715
|
// Optional list with default
|
|
642
|
-
v.array(v.string()).default([]).optional()
|
|
716
|
+
v.array(v.string()).default([]).optional();
|
|
643
717
|
```
|
|
644
718
|
|
|
645
719
|
|
|
@@ -656,10 +730,12 @@ These return a new `ObjectValidator` with a transformed schema. The TypeScript i
|
|
|
656
730
|
### `.extend(schemaOrValidator)` — add fields, keep config
|
|
657
731
|
|
|
658
732
|
```ts
|
|
659
|
-
const baseUser = v
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
733
|
+
const baseUser = v
|
|
734
|
+
.object({
|
|
735
|
+
name: v.string(),
|
|
736
|
+
email: v.string().email(),
|
|
737
|
+
})
|
|
738
|
+
.allowUnknown();
|
|
663
739
|
|
|
664
740
|
const adminUser = baseUser.extend({
|
|
665
741
|
role: v.string().oneOf(["admin", "superadmin"]),
|
|
@@ -673,10 +749,12 @@ Accepts a plain schema object **or** another `ObjectValidator` (only its schema
|
|
|
673
749
|
|
|
674
750
|
```ts
|
|
675
751
|
const base = v.object({ name: v.string() }).allowUnknown();
|
|
676
|
-
const audit = v
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
752
|
+
const audit = v
|
|
753
|
+
.object({
|
|
754
|
+
createdAt: v.date(),
|
|
755
|
+
updatedAt: v.date(),
|
|
756
|
+
})
|
|
757
|
+
.stripUnknown();
|
|
680
758
|
|
|
681
759
|
const merged = base.merge(audit);
|
|
682
760
|
// type: { name; createdAt; updatedAt }
|
|
@@ -713,8 +791,8 @@ Returns `ObjectValidator<Omit<TSchema, K>>`. Inverse of `.pick`. Keeps original
|
|
|
713
791
|
### `.partial(...keys?)` — mark fields optional
|
|
714
792
|
|
|
715
793
|
```ts
|
|
716
|
-
fullUser.partial()
|
|
717
|
-
fullUser.partial("password")
|
|
794
|
+
fullUser.partial(); // every field becomes optional
|
|
795
|
+
fullUser.partial("password"); // only `password` becomes optional
|
|
718
796
|
```
|
|
719
797
|
|
|
720
798
|
Walks the schema and applies `.optional()` to each named field (or all if no keys given). `Infer<>` makes those keys optional.
|
|
@@ -722,7 +800,7 @@ Walks the schema and applies `.optional()` to each named field (or all if no key
|
|
|
722
800
|
### `.requiredFields(...keys?)` — mark fields required
|
|
723
801
|
|
|
724
802
|
```ts
|
|
725
|
-
const partialUser = fullUser.partial();
|
|
803
|
+
const partialUser = fullUser.partial(); // all optional
|
|
726
804
|
const updateUser = partialUser.requiredFields("id", "email");
|
|
727
805
|
// id and email are required again, others stay optional
|
|
728
806
|
```
|
|
@@ -733,29 +811,31 @@ Inverse of `.partial`.
|
|
|
733
811
|
|
|
734
812
|
By default, extra keys in input are silently dropped from `data` (no error, no forward).
|
|
735
813
|
|
|
736
|
-
| Method
|
|
737
|
-
|
|
738
|
-
| `.allowUnknown(allow = true)` | extra **direct-child** keys forward as-is
|
|
739
|
-
| `.stripUnknown()`
|
|
740
|
-
| `.allow(...keys)`
|
|
814
|
+
| Method | Effect |
|
|
815
|
+
| ----------------------------- | ------------------------------------------------------- |
|
|
816
|
+
| `.allowUnknown(allow = true)` | extra **direct-child** keys forward as-is |
|
|
817
|
+
| `.stripUnknown()` | explicit drop (mutator-based — affects `data` shape) |
|
|
818
|
+
| `.allow(...keys)` | whitelist specific extras to forward without validation |
|
|
741
819
|
|
|
742
820
|
`.allowUnknown()` only affects direct children — nested objects keep their own policies. For a fully permissive object including nested children, set `.allowUnknown()` on each level.
|
|
743
821
|
|
|
744
822
|
### Output DTOs — stripping extras instead of rejecting them
|
|
745
823
|
|
|
746
|
-
The default is to **reject** unknown keys, which is what you want for inbound payloads. For an
|
|
824
|
+
The default is to **reject** unknown keys, which is what you want for inbound payloads. For an _outbound_ DTO — where the record legitimately carries internal fields you simply don't want to expose — use `.stripUnknown()`, which drops them and returns the clean object:
|
|
747
825
|
|
|
748
826
|
```ts
|
|
749
|
-
const publicArticle = v
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
827
|
+
const publicArticle = v
|
|
828
|
+
.object({
|
|
829
|
+
id: v.string(),
|
|
830
|
+
title: v.string(),
|
|
831
|
+
})
|
|
832
|
+
.stripUnknown();
|
|
753
833
|
|
|
754
834
|
const { isValid, data } = await v.validate(publicArticle, record);
|
|
755
835
|
// record had version / authorId / status; data has only id + title
|
|
756
836
|
```
|
|
757
837
|
|
|
758
|
-
Without `.stripUnknown()` the same record fails with
|
|
838
|
+
Without `.stripUnknown()` the same record fails with _"The schema contains unknown keys: …"_ rather than being cleaned.
|
|
759
839
|
|
|
760
840
|
:::note[`data` is `undefined` on failure — since 4.9.2]
|
|
761
841
|
A failed validation returns `data: undefined`, never the input it rejected. Before 4.9.2 an `object` returned the raw input on failure (while `discriminatedUnion` returned `undefined`), so a caller who read `data` without branching on `isValid` forwarded the exact fields the schema existed to exclude. Always branch on `isValid`.
|
|
@@ -763,8 +843,8 @@ A failed validation returns `data: undefined`, never the input it rejected. Befo
|
|
|
763
843
|
|
|
764
844
|
## Object-level mutators
|
|
765
845
|
|
|
766
|
-
| Method
|
|
767
|
-
|
|
846
|
+
| Method | Args | Effect |
|
|
847
|
+
| ------------------- | -------------- | ------------------------------------------------ |
|
|
768
848
|
| `.trim(recursive?)` | default `true` | recursively trim string values across the object |
|
|
769
849
|
|
|
770
850
|
For per-field mutators, attach on the field validator instead.
|
|
@@ -777,7 +857,7 @@ The cross-field methods (`.sameAs`, `.requiredIf`, `.requiredWith`, etc.) are **
|
|
|
777
857
|
v.object({
|
|
778
858
|
password: v.string(),
|
|
779
859
|
passwordConfirm: v.string().sameAs("password"),
|
|
780
|
-
})
|
|
860
|
+
});
|
|
781
861
|
```
|
|
782
862
|
|
|
783
863
|
If you call cross-field rules outside a `v.object`, sibling resolution silently passes — there's nobody to compare against.
|
|
@@ -785,7 +865,7 @@ If you call cross-field rules outside a `v.object`, sibling resolution silently
|
|
|
785
865
|
## `Infer<>` semantics
|
|
786
866
|
|
|
787
867
|
```ts
|
|
788
|
-
Infer<typeof userSchema
|
|
868
|
+
Infer<typeof userSchema>;
|
|
789
869
|
// { reqField: T; optField?: T }
|
|
790
870
|
```
|
|
791
871
|
|
|
@@ -818,10 +898,12 @@ const timestamps = v.object({
|
|
|
818
898
|
const userWithAudit = baseUser.extend(timestamps);
|
|
819
899
|
|
|
820
900
|
// Permissive container that forwards extras
|
|
821
|
-
const eventEnvelope = v
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}).allowUnknown()
|
|
901
|
+
const eventEnvelope = v
|
|
902
|
+
.object({
|
|
903
|
+
type: v.literal("user.created", "user.updated"),
|
|
904
|
+
payload: v.object({}).allowUnknown(),
|
|
905
|
+
})
|
|
906
|
+
.allowUnknown();
|
|
825
907
|
|
|
826
908
|
// Confirmation pattern with omit
|
|
827
909
|
const signupSchema = v.object({
|
|
@@ -858,7 +940,7 @@ Seal exposes a plugin system so you can add validator methods without forking th
|
|
|
858
940
|
import type { SealPlugin } from "@warlock.js/seal";
|
|
859
941
|
|
|
860
942
|
type SealPlugin = {
|
|
861
|
-
name: string;
|
|
943
|
+
name: string; // unique identifier — duplicates warn and skip install
|
|
862
944
|
version?: string;
|
|
863
945
|
description?: string;
|
|
864
946
|
install: (context: { name: string; version?: string }) => void | Promise<void>;
|
|
@@ -896,8 +978,8 @@ import { registerPlugin, unregisterPlugin, hasPlugin, getInstalledPlugins } from
|
|
|
896
978
|
await registerPlugin(slugPlugin);
|
|
897
979
|
// console.warn if "slug" is already installed; otherwise install() runs and the plugin is tracked.
|
|
898
980
|
|
|
899
|
-
hasPlugin("slug");
|
|
900
|
-
getInstalledPlugins();
|
|
981
|
+
hasPlugin("slug"); // → true
|
|
982
|
+
getInstalledPlugins(); // → [slugPlugin]
|
|
901
983
|
|
|
902
984
|
await unregisterPlugin("slug");
|
|
903
985
|
// runs slugPlugin.uninstall?.(); removes from registry.
|
|
@@ -1000,8 +1082,8 @@ For a method that creates a **new validator** (not chained from an existing one)
|
|
|
1000
1082
|
## Introspection — checking what's loaded
|
|
1001
1083
|
|
|
1002
1084
|
```ts
|
|
1003
|
-
hasPlugin("slug");
|
|
1004
|
-
getInstalledPlugins();
|
|
1085
|
+
hasPlugin("slug"); // boolean
|
|
1086
|
+
getInstalledPlugins(); // SealPlugin[]
|
|
1005
1087
|
```
|
|
1006
1088
|
|
|
1007
1089
|
Use these in startup diagnostics or in tests that need to assert a plugin is registered before exercising a method that depends on it.
|
|
@@ -1014,6 +1096,40 @@ Use these in startup diagnostics or in tests that need to assert a plugin is reg
|
|
|
1014
1096
|
- Don't ship a plugin that overrides a built-in method without a clear reason. If you must, `uninstall` should restore the original — but the better path is a different method name.
|
|
1015
1097
|
- Don't author one plugin per method. Group related methods (e.g. "country-specific validators") into one plugin so the install/uninstall lifecycle is coherent.
|
|
1016
1098
|
|
|
1099
|
+
## Returning validated rule output
|
|
1100
|
+
|
|
1101
|
+
A custom rule's success result can contain `data` to replace the current value:
|
|
1102
|
+
`{ isValid: true, data: replacement }`. Later rules, transformers and refinements
|
|
1103
|
+
receive that replacement. Omitting `data` preserves the current value; a present
|
|
1104
|
+
`data: undefined` explicitly clears it. This is also how a union preserves the
|
|
1105
|
+
selected branch's validated defaults, catches and child transformations.
|
|
1106
|
+
|
|
1107
|
+
## Reporting several errors from one rule
|
|
1108
|
+
|
|
1109
|
+
A failed rule result can carry `errors: [{ error, path }, ...]` to report more than
|
|
1110
|
+
one error, each at its own path; every entry gets the rule's `name` as its `type`.
|
|
1111
|
+
`v.union` uses this to relay the closest branch's errors. Return as many as fit the
|
|
1112
|
+
caller's `context.configurations?.firstErrorOnly` (default `true`: one).
|
|
1113
|
+
|
|
1114
|
+
```ts
|
|
1115
|
+
import { invalidRule, type SchemaRule } from "@warlock.js/seal";
|
|
1116
|
+
|
|
1117
|
+
const bothDates: SchemaRule = {
|
|
1118
|
+
name: "bothDates",
|
|
1119
|
+
async validate(value: { from?: unknown; to?: unknown }, context) {
|
|
1120
|
+
const missing = (["from", "to"] as const).filter((key) => !value?.[key]);
|
|
1121
|
+
if (missing.length === 0) return { isValid: true };
|
|
1122
|
+
|
|
1123
|
+
const failed = invalidRule(this, context);
|
|
1124
|
+
const errors = missing.map((key) => ({
|
|
1125
|
+
error: `The ${key} date is required`,
|
|
1126
|
+
path: context.path ? `${context.path}.${key}` : key,
|
|
1127
|
+
}));
|
|
1128
|
+
return { ...failed, errors };
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1017
1133
|
|
|
1018
1134
|
## generate-json-schema `@warlock.js/seal/generate-json-schema/SKILL.md`
|
|
1019
1135
|
|
|
@@ -1048,19 +1164,19 @@ userSchema.toJsonSchema("draft-2020-12");
|
|
|
1048
1164
|
|
|
1049
1165
|
```ts
|
|
1050
1166
|
type JsonSchemaTarget =
|
|
1051
|
-
| "draft-2020-12"
|
|
1052
|
-
| "draft-07"
|
|
1053
|
-
| "openapi-3.0"
|
|
1054
|
-
| "openai-strict"
|
|
1167
|
+
| "draft-2020-12" // default — modern JSON Schema
|
|
1168
|
+
| "draft-07" // older tooling, Swagger 2.0
|
|
1169
|
+
| "openapi-3.0" // uses { nullable: true } instead of type unions
|
|
1170
|
+
| "openai-strict"; // OpenAI Structured Outputs strict mode
|
|
1055
1171
|
```
|
|
1056
1172
|
|
|
1057
1173
|
Pick by consumer:
|
|
1058
1174
|
|
|
1059
|
-
| Consumer
|
|
1060
|
-
|
|
|
1061
|
-
| Modern tooling, no specific reason otherwise
|
|
1062
|
-
| Swagger 2.0 / older OpenAPI / older form builders
|
|
1063
|
-
| OpenAPI 3.0 spec (uses `nullable: true`)
|
|
1175
|
+
| Consumer | Target |
|
|
1176
|
+
| --------------------------------------------------------------- | ----------------- |
|
|
1177
|
+
| Modern tooling, no specific reason otherwise | `"draft-2020-12"` |
|
|
1178
|
+
| Swagger 2.0 / older OpenAPI / older form builders | `"draft-07"` |
|
|
1179
|
+
| OpenAPI 3.0 spec (uses `nullable: true`) | `"openapi-3.0"` |
|
|
1064
1180
|
| OpenAI `response_format: { type: "json_schema", strict: true }` | `"openai-strict"` |
|
|
1065
1181
|
|
|
1066
1182
|
## OpenAI structured outputs (`openai-strict`)
|
|
@@ -1141,8 +1257,8 @@ Cleanly mapped:
|
|
|
1141
1257
|
|
|
1142
1258
|
Some seal constructs have no JSON Schema representation:
|
|
1143
1259
|
|
|
1144
|
-
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the
|
|
1145
|
-
- **Transformers and mutators** — output reshaping doesn't appear in the schema; the schema reflects
|
|
1260
|
+
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the _shape_, not the inter-field invariants.
|
|
1261
|
+
- **Transformers and mutators** — output reshaping doesn't appear in the schema; the schema reflects _post-mutator, pre-transformer_ shape (since that's what rules see and what the LLM is asked to produce for `openai-strict`).
|
|
1146
1262
|
- **`v.computed` / `v.managed`** — **skipped** entirely by the parent `v.object`; they never appear in `properties`. They aren't part of the data contract. (Calling `.toJsonSchema()` directly on one throws — the parent object is responsible for skipping them.)
|
|
1147
1263
|
- **`v.instanceof(Ctor)`** — produces `{}`. Class identity isn't expressible. For `File`, attach `{ type: "string", format: "binary" }` manually after generation if needed for OpenAPI.
|
|
1148
1264
|
- **`v.any()`** — produces `{}` deliberately (any value is valid).
|
|
@@ -1151,10 +1267,10 @@ Boolean rules `accepted` / `declined` and similar coercion-style rules are also
|
|
|
1151
1267
|
|
|
1152
1268
|
## When the generated schema rejects valid data
|
|
1153
1269
|
|
|
1154
|
-
If the schema validator itself accepts data but the
|
|
1270
|
+
If the schema validator itself accepts data but the _generated_ JSON Schema rejects the same data downstream, the cause is usually one of:
|
|
1155
1271
|
|
|
1156
1272
|
- **Cross-field rule.** The generated schema doesn't enforce it, but a separate consumer might. (Or the runtime check fired at a different stage.)
|
|
1157
|
-
- **Transformer running on the wrong side.** The schema describes the input shape (or strict-mode normalized form). If your transformer reshapes `Date` to ISO string for `data`, the
|
|
1273
|
+
- **Transformer running on the wrong side.** The schema describes the input shape (or strict-mode normalized form). If your transformer reshapes `Date` to ISO string for `data`, the _input_ still needs to be a Date-parseable thing.
|
|
1158
1274
|
- **`openai-strict` quirk.** Optional fields show as `["T", "null"]` rather than omitted — if the model omits them entirely (without sending `null`), strict mode fails. The fix is on the prompt side: tell the model to send `null` for unused fields.
|
|
1159
1275
|
|
|
1160
1276
|
## Cost note
|
|
@@ -1175,13 +1291,13 @@ description: 'Read `ValidationResult` — `isValid`, `errors[]`, `data`. Branch
|
|
|
1175
1291
|
|
|
1176
1292
|
```ts
|
|
1177
1293
|
type TypedValidationResult<TData> =
|
|
1178
|
-
| { isValid: true; data: TData; errors: ValidationError[] }
|
|
1294
|
+
| { isValid: true; data: TData; errors: ValidationError[] } // TData = Infer.Output<typeof schema>
|
|
1179
1295
|
| { isValid: false; data: undefined; errors: ValidationError[] };
|
|
1180
1296
|
|
|
1181
1297
|
type ValidationError = {
|
|
1182
|
-
type: string;
|
|
1183
|
-
error: string;
|
|
1184
|
-
input: string;
|
|
1298
|
+
type: string; // rule type that failed — "required", "email", "min", "string", ...
|
|
1299
|
+
error: string; // resolved message (translated, with attribute substitution)
|
|
1300
|
+
input: string; // input field name — "email" or "address.city"
|
|
1185
1301
|
};
|
|
1186
1302
|
```
|
|
1187
1303
|
|
|
@@ -1200,7 +1316,7 @@ const schema = v.object({
|
|
|
1200
1316
|
const result = await validate(schema, input);
|
|
1201
1317
|
|
|
1202
1318
|
if (result.isValid) {
|
|
1203
|
-
return result.data;
|
|
1319
|
+
return result.data; // typed as the inferred shape (post-transformer)
|
|
1204
1320
|
}
|
|
1205
1321
|
|
|
1206
1322
|
return {
|
|
@@ -1215,15 +1331,13 @@ return {
|
|
|
1215
1331
|
const result = await validate(schema, input);
|
|
1216
1332
|
|
|
1217
1333
|
if (!result.isValid) {
|
|
1218
|
-
const emailMissing = result.errors.find(
|
|
1219
|
-
e => e.input === "email" && e.type === "required",
|
|
1220
|
-
);
|
|
1334
|
+
const emailMissing = result.errors.find((e) => e.input === "email" && e.type === "required");
|
|
1221
1335
|
if (emailMissing) {
|
|
1222
1336
|
return { redirect: "/signup", reason: "no email" };
|
|
1223
1337
|
}
|
|
1224
1338
|
|
|
1225
1339
|
const ageInvalid = result.errors.find(
|
|
1226
|
-
e => e.input === "age" && (e.type === "int" || e.type === "min"),
|
|
1340
|
+
(e) => e.input === "age" && (e.type === "int" || e.type === "min"),
|
|
1227
1341
|
);
|
|
1228
1342
|
if (ageInvalid) {
|
|
1229
1343
|
return { error: "Age must be 13 or older" };
|
|
@@ -1239,18 +1353,20 @@ These are the strings you'll see in `error.type`:
|
|
|
1239
1353
|
|
|
1240
1354
|
The `type` is the rule's own `name` field — not the method you called. Several methods map to one shared rule name (e.g. `.min()` / `.minLength()` on a string both surface as `minLength`; `.sameAs()` surfaces as `equalsField`). The table below lists the actual `type` strings:
|
|
1241
1355
|
|
|
1242
|
-
| Type
|
|
1243
|
-
|
|
|
1244
|
-
| `required`, `present`
|
|
1245
|
-
| `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless`
|
|
1246
|
-
| `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date`
|
|
1247
|
-
| `minLength`, `maxLength`, `betweenLength`, `length`
|
|
1248
|
-
| `min`, `max`, `betweenNumbers`
|
|
1249
|
-
| `email`, `url`, `uuid`, `pattern`, `matches`
|
|
1250
|
-
| `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues`
|
|
1251
|
-
| `instanceof`
|
|
1252
|
-
| `equalsField`, `notEqualsField`
|
|
1253
|
-
| `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`)
|
|
1356
|
+
| Type | Produced by |
|
|
1357
|
+
| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
1358
|
+
| `required`, `present` | `.required()`, `.present()` (note: `.optional()` has no error type — it clears the required rule) |
|
|
1359
|
+
| `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless` | conditional required methods |
|
|
1360
|
+
| `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date` | type guards from `matchesType` |
|
|
1361
|
+
| `minLength`, `maxLength`, `betweenLength`, `length` | string / array length rules (`.min`/`.max` on a string alias to `minLength`/`maxLength`) |
|
|
1362
|
+
| `min`, `max`, `betweenNumbers` | number range rules (`.min`/`.max`/`.between` on a number) |
|
|
1363
|
+
| `email`, `url`, `uuid`, `pattern`, `matches` | string format rules (`.pattern()` → `pattern`) |
|
|
1364
|
+
| `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues` | value-membership rules (`.oneOf` aliases `in`; `.notIn`/`.forbids` → `notAllowedValues`; `.allowsOnly` → `allowedValues`) |
|
|
1365
|
+
| `instanceof` | `v.instanceof(Ctor)` |
|
|
1366
|
+
| `equalsField`, `notEqualsField` | cross-field equality (`.sameAs` → `equalsField`; `.differentFrom` → `notEqualsField`) |
|
|
1367
|
+
| `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`) |
|
|
1368
|
+
|
|
1369
|
+
`v.union(...)` reports `union`: when no branch passes, it relays the closest branch's errors (fewest mismatched literals, then fewest errors, then deepest path) at their own paths, all typed `union`. A custom rule can do the same by returning `errors: [{ error, path }, ...]` with its failed result.
|
|
1254
1370
|
|
|
1255
1371
|
If you write custom rules, the `type` name you set on the rule object is what shows up here. Pick stable, kebab-or-camel-case names — they become a public API.
|
|
1256
1372
|
|
|
@@ -1282,7 +1398,7 @@ For attribute display names ("email" → "Email Address"), use `.attributes({ em
|
|
|
1282
1398
|
|
|
1283
1399
|
## When to throw
|
|
1284
1400
|
|
|
1285
|
-
Don't wrap `validate()` in try/catch for
|
|
1401
|
+
Don't wrap `validate()` in try/catch for _validation_ failures — those land in `result.errors`. The only thing that _throws_ is a programming bug:
|
|
1286
1402
|
|
|
1287
1403
|
- A rule's callback threw (e.g. you wrote `async validate() { throw new Error(...) }`).
|
|
1288
1404
|
- A transformer threw on output.
|
|
@@ -1298,7 +1414,7 @@ If you're surfacing seal errors through HTTP / RPC, the typical shape is:
|
|
|
1298
1414
|
if (!result.isValid) {
|
|
1299
1415
|
return reply.code(422).send({
|
|
1300
1416
|
error: "validation_failed",
|
|
1301
|
-
fields: result.errors.map(e => ({
|
|
1417
|
+
fields: result.errors.map((e) => ({
|
|
1302
1418
|
field: e.input,
|
|
1303
1419
|
type: e.type,
|
|
1304
1420
|
message: e.error,
|
|
@@ -1321,7 +1437,7 @@ description: "@warlock.js/seal overview for orientation and choosing the focused
|
|
|
1321
1437
|
|
|
1322
1438
|
# `@warlock.js/seal` — overview
|
|
1323
1439
|
|
|
1324
|
-
A type-safe, framework-agnostic validation library. You build a schema with the `v` factory, run it with `validate(schema, data)`, and pull the static type out with `Infer<typeof schema>`. One schema is the runtime check
|
|
1440
|
+
A type-safe, framework-agnostic validation library. You build a schema with the `v` factory, run it with `validate(schema, data)`, and pull the static type out with `Infer<typeof schema>`. One schema is the runtime check _and_ the TypeScript type _and_ (optionally) a JSON Schema.
|
|
1325
1441
|
|
|
1326
1442
|
Standalone — no framework required. Ships built-in with `@warlock.js/core`, which adds framework-specific validators (file uploads, database existence/uniqueness rules) on top.
|
|
1327
1443
|
|
|
@@ -1344,31 +1460,39 @@ Eight task skills. Most schemas only need `seal-basics` + `pick-seal-primitive`
|
|
|
1344
1460
|
### Foundations
|
|
1345
1461
|
|
|
1346
1462
|
#### `seal-basics`
|
|
1463
|
+
|
|
1347
1464
|
Start here. The `v` factory, `validate(schema, data)`, and `Infer<typeof schema>`.
|
|
1348
1465
|
|
|
1349
1466
|
### Building schemas
|
|
1350
1467
|
|
|
1351
1468
|
#### `pick-seal-primitive`
|
|
1469
|
+
|
|
1352
1470
|
Choose the right primitive — `string` / `int` / `literal` / `date` / `enum` / `computed` / `managed` / `instanceof` / `any`. Covers the close calls (`string` vs `scalar`, `int` vs `number`, `literal` vs `enum`).
|
|
1353
1471
|
|
|
1354
1472
|
#### `define-structural-shape`
|
|
1473
|
+
|
|
1355
1474
|
Compose `v.object` / `v.array` / `v.record` / `v.tuple` / `v.union` / `v.discriminatedUnion` / `v.lazy` — object schemas, dynamic-keyed records, tagged unions, recursive shapes.
|
|
1356
1475
|
|
|
1357
1476
|
#### `compose-seal-modifiers`
|
|
1477
|
+
|
|
1358
1478
|
Cross-cutting modifiers — `.optional` / `.nullable` / `.default` / `.catch` / `.omit` / membership rules — plus the mutator-vs-transformer pipeline and `Infer.Input` vs `Infer.Output`.
|
|
1359
1479
|
|
|
1360
1480
|
### Output + interop
|
|
1361
1481
|
|
|
1362
1482
|
#### `handle-seal-errors`
|
|
1483
|
+
|
|
1363
1484
|
Read a `ValidationResult` — `isValid`, `errors[]`, `data`. Branch on `error.type`, customize messages, hook translation.
|
|
1364
1485
|
|
|
1365
1486
|
#### `generate-json-schema`
|
|
1487
|
+
|
|
1366
1488
|
`schema.toJsonSchema(target)` — draft-2020-12 / draft-07 / openapi-3.0 / openai-strict. For OpenAI structured outputs, OpenAPI specs, any JSON-Schema consumer.
|
|
1367
1489
|
|
|
1368
1490
|
#### `bridge-standard-schema`
|
|
1369
|
-
|
|
1491
|
+
|
|
1492
|
+
Standard Schema interop — why a `StandardSchemaV1<T>` slot might reject a schema, object input/output class parameters and primitive factory intersections, Cascade `Model<TSchema>` variance. For migrating off `as unknown as` casts.
|
|
1370
1493
|
|
|
1371
1494
|
#### `extend-seal-with-plugins`
|
|
1495
|
+
|
|
1372
1496
|
Author a plugin to add validator methods (`.slug`, `.postalCode`, …) — the `SealPlugin` shape, `registerPlugin` lifecycle, TS prototype augmentation.
|
|
1373
1497
|
|
|
1374
1498
|
## Configuration
|
|
@@ -1397,25 +1521,25 @@ description: 'Pick the right `v` factory primitive — string / int / literal /
|
|
|
1397
1521
|
|
|
1398
1522
|
# Picking the right primitive
|
|
1399
1523
|
|
|
1400
|
-
This is the orientation skill —
|
|
1524
|
+
This is the orientation skill — _which_ primitive for _which_ job. For the chainable methods on each (`.email()`, `.min()`, `.between()`, etc.), load the matching `*-methods.md` reference file in this skill folder.
|
|
1401
1525
|
|
|
1402
1526
|
## Strings
|
|
1403
1527
|
|
|
1404
1528
|
```ts
|
|
1405
|
-
v.string()
|
|
1406
|
-
v.email()
|
|
1407
|
-
v.enum(["a", "b"])
|
|
1529
|
+
v.string(); // type: string — full surface in string-methods.md
|
|
1530
|
+
v.email(); // shorthand for v.string().email()
|
|
1531
|
+
v.enum(["a", "b"]); // type: "a" | "b" — runs as v.string().oneOf, but the factory overload preserves the literal union
|
|
1408
1532
|
```
|
|
1409
1533
|
|
|
1410
1534
|
Reach for `v.string()` for any text input. `v.email()` is just sugar — switch back to `v.string().email().min(...)` when you need extra rules.
|
|
1411
1535
|
|
|
1412
1536
|
## Numbers — pick by what you accept
|
|
1413
1537
|
|
|
1414
|
-
| Validator
|
|
1415
|
-
|
|
1416
|
-
| `v.number()`
|
|
1417
|
-
| `v.int()`
|
|
1418
|
-
| `v.float()`
|
|
1538
|
+
| Validator | Accepts | When |
|
|
1539
|
+
| ------------- | ------------------------- | ----------------------------------------------------------- |
|
|
1540
|
+
| `v.number()` | any finite number | accepts both integers and floats |
|
|
1541
|
+
| `v.int()` | integers only | rejects `1.5` |
|
|
1542
|
+
| `v.float()` | finite, non-integer | rejects `1` |
|
|
1419
1543
|
| `v.numeric()` | numeric strings + numbers | form/query inputs that arrive as `"42"` — coerces to number |
|
|
1420
1544
|
|
|
1421
1545
|
All four share the same chain surface (see [`number-methods.md`](./number-methods.md)). Picking is about input acceptance, not chain power.
|
|
@@ -1423,8 +1547,8 @@ All four share the same chain surface (see [`number-methods.md`](./number-method
|
|
|
1423
1547
|
## Booleans & scalars
|
|
1424
1548
|
|
|
1425
1549
|
```ts
|
|
1426
|
-
v.boolean()
|
|
1427
|
-
v.scalar()
|
|
1550
|
+
v.boolean(); // type: boolean — adds .accepted() / .declined() for form-style inputs
|
|
1551
|
+
v.scalar(); // type: string | number | boolean — usually a smell pointing at a missing discriminator
|
|
1428
1552
|
```
|
|
1429
1553
|
|
|
1430
1554
|
Use `v.scalar()` only when the field truly accepts any of the three primitives. If it's "one of N specific values across types", `v.literal(...)` is cleaner.
|
|
@@ -1432,8 +1556,8 @@ Use `v.scalar()` only when the field truly accepts any of the three primitives.
|
|
|
1432
1556
|
## Dates
|
|
1433
1557
|
|
|
1434
1558
|
```ts
|
|
1435
|
-
v.date()
|
|
1436
|
-
v.instanceof(Date)
|
|
1559
|
+
v.date(); // type: Date — normalizes strings/timestamps to Date, rich rule surface
|
|
1560
|
+
v.instanceof(Date); // type: Date — raw instanceof, no normalization, no rules
|
|
1437
1561
|
```
|
|
1438
1562
|
|
|
1439
1563
|
`v.date()` is the right tool 99% of the time — it ships `.min/.max/.before/.after/.weekDay/.minAge/...` and a built-in mutator that parses strings. `v.instanceof(Date)` is the escape hatch when you specifically need strict instance identity with zero coercion.
|
|
@@ -1441,14 +1565,14 @@ v.instanceof(Date) // type: Date — raw instanceof, no normalization, no
|
|
|
1441
1565
|
## Literals & instances
|
|
1442
1566
|
|
|
1443
1567
|
```ts
|
|
1444
|
-
v.literal("items")
|
|
1445
|
-
v.literal("draft", "published", "archived") // type: "draft" | "published" | "archived"
|
|
1446
|
-
v.literal(1, 2, 3)
|
|
1447
|
-
v.literal(true)
|
|
1568
|
+
v.literal("items"); // type: "items"
|
|
1569
|
+
v.literal("draft", "published", "archived"); // type: "draft" | "published" | "archived"
|
|
1570
|
+
v.literal(1, 2, 3); // type: 1 | 2 | 3
|
|
1571
|
+
v.literal(true); // type: true
|
|
1448
1572
|
|
|
1449
|
-
v.instanceof(File)
|
|
1450
|
-
v.instanceof(Buffer)
|
|
1451
|
-
v.instanceof(MyClass)
|
|
1573
|
+
v.instanceof(File); // type: File
|
|
1574
|
+
v.instanceof(Buffer); // type: Buffer
|
|
1575
|
+
v.instanceof(MyClass); // type: MyClass
|
|
1452
1576
|
```
|
|
1453
1577
|
|
|
1454
1578
|
**`v.literal` vs `v.string().oneOf([...])` vs `v.enum([...])`:**
|
|
@@ -1457,20 +1581,20 @@ v.instanceof(MyClass) // type: MyClass
|
|
|
1457
1581
|
- `v.string().oneOf(["a", "b"])` infers as `string` (loses literal types). Use when broad type is fine.
|
|
1458
1582
|
- `v.enum(["a", "b"])` runs the same `oneOf` rule at runtime (it builds a `StringValidator().oneOf(...)`), but the `v.enum` factory overload **preserves the literal union** — it infers `"a" | "b"`, not `string`. Pass a TS enum object (`v.enum(Direction)`) and it uses `Object.values`, inferring `Direction[keyof Direction]`.
|
|
1459
1583
|
|
|
1460
|
-
`**`v.literal("")
|
|
1584
|
+
`**`v.literal("")`— the empty string is a value, not an absence.**`""` is accepted when it is one of the configured literals, and the field is still required to be _present_:
|
|
1461
1585
|
|
|
1462
1586
|
```ts
|
|
1463
1587
|
const decorative = v.object({ decorative: v.literal(true), alt: v.literal("") });
|
|
1464
1588
|
|
|
1465
|
-
await v.validate(decorative, { decorative: true, alt: "" });
|
|
1466
|
-
await v.validate(decorative, { decorative: true, alt: "text" });
|
|
1467
|
-
await v.validate(decorative, { decorative: true });
|
|
1589
|
+
await v.validate(decorative, { decorative: true, alt: "" }); // valid
|
|
1590
|
+
await v.validate(decorative, { decorative: true, alt: "text" }); // invalid — not the literal
|
|
1591
|
+
await v.validate(decorative, { decorative: true }); // invalid — key missing
|
|
1468
1592
|
```
|
|
1469
1593
|
|
|
1470
1594
|
That distinction matters wherever "empty" and "absent" mean different things — `alt=""` marks an image as decorative to a screen reader, while a missing `alt` is an authoring mistake. Add `.optional()` only when a missing key really is acceptable; a present value must still match the literal.
|
|
1471
1595
|
|
|
1472
1596
|
:::note[Fixed in 4.9.2]
|
|
1473
|
-
Before 4.9.2 `v.literal("")` always failed with
|
|
1597
|
+
Before 4.9.2 `v.literal("")` always failed with _"is required"_, and `.optional()` looked like a workaround while actually disabling the literal check — accepting `""`, `null`, and a missing key alike. `v.literal(0)` and `v.literal(false)` were never affected.
|
|
1474
1598
|
:::
|
|
1475
1599
|
|
|
1476
1600
|
:::note[Fixed again in 5.30]
|
|
@@ -1482,7 +1606,7 @@ Before 4.9.2 `v.literal("")` always failed with *"is required"*, and `.optional(
|
|
|
1482
1606
|
## `v.any` — escape hatch
|
|
1483
1607
|
|
|
1484
1608
|
```ts
|
|
1485
|
-
v.any()
|
|
1609
|
+
v.any(); // type: any — skips validation entirely
|
|
1486
1610
|
```
|
|
1487
1611
|
|
|
1488
1612
|
Reach for it when you genuinely don't care about the shape. Usually a smell — search PRs for it and ask whether a real schema would catch a class of bugs.
|
|
@@ -1504,26 +1628,26 @@ v.object({
|
|
|
1504
1628
|
- **`v.computed`** runs after sibling validation; callback signature is `(data, context)` — `data` is the validated sibling object. Use for derived values (full name, hash of fields, computed totals). An optional second arg validates the result: `v.computed<string>(cb, v.string().min(3))`.
|
|
1505
1629
|
- **`v.managed`** runs from `SchemaContext` only — callback signature is `(context)`. Caller-supplied extras passed to `validate(schema, data, { context })` land on `context.context`. Use for framework-injected values — timestamps, current user, request id. The callback is optional (`v.managed()`) for values the framework injects without a generator.
|
|
1506
1630
|
|
|
1507
|
-
Both are **skipped** when their parent `v.object` generates JSON Schema — they never appear in `properties`, since they're runtime-only and not part of the JSON contract an LLM or external API consumer reads. Calling `.toJsonSchema()`
|
|
1631
|
+
Both are **skipped** when their parent `v.object` generates JSON Schema — they never appear in `properties`, since they're runtime-only and not part of the JSON contract an LLM or external API consumer reads. Calling `.toJsonSchema()` _directly_ on a `v.computed` / `v.managed` validator **throws** (it's a programming error — let the parent object skip them).
|
|
1508
1632
|
|
|
1509
1633
|
## Quick map — "I need to validate…"
|
|
1510
1634
|
|
|
1511
|
-
| Need
|
|
1512
|
-
|
|
1513
|
-
| Email
|
|
1514
|
-
| URL
|
|
1515
|
-
| UUID
|
|
1516
|
-
| Number 0–100
|
|
1517
|
-
| Positive integer
|
|
1518
|
-
| One of N constants
|
|
1519
|
-
| One of TS enum values
|
|
1520
|
-
| Date in the past
|
|
1521
|
-
| File upload
|
|
1522
|
-
| Class instance (not Date)
|
|
1523
|
-
| Discriminated union
|
|
1524
|
-
| Derived value (computed from siblings) | `v.computed<T>(callback)`
|
|
1525
|
-
| Framework-injected value
|
|
1526
|
-
| Free-form / pass-through
|
|
1635
|
+
| Need | Reach for |
|
|
1636
|
+
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
1637
|
+
| Email | `v.string().email()` or `v.email()` |
|
|
1638
|
+
| URL | `v.string().url()` |
|
|
1639
|
+
| UUID | `v.string().uuid()` (see [`string-methods.md`](./string-methods.md)) |
|
|
1640
|
+
| Number 0–100 | `v.number().between(0, 100)` |
|
|
1641
|
+
| Positive integer | `v.int().positive()` |
|
|
1642
|
+
| One of N constants | `v.literal(...values)` |
|
|
1643
|
+
| One of TS enum values | `v.enum(MyEnum)` |
|
|
1644
|
+
| Date in the past | `v.date().past()` |
|
|
1645
|
+
| File upload | `v.instanceof(File)` |
|
|
1646
|
+
| Class instance (not Date) | `v.instanceof(Ctor)` |
|
|
1647
|
+
| Discriminated union | see the `define-structural-shape` topic |
|
|
1648
|
+
| Derived value (computed from siblings) | `v.computed<T>(callback)` |
|
|
1649
|
+
| Framework-injected value | `v.managed<T>(callback)` |
|
|
1650
|
+
| Free-form / pass-through | `v.any()` (only when you've thought about it) |
|
|
1527
1651
|
|
|
1528
1652
|
## Method-surface reference
|
|
1529
1653
|
|
|
@@ -1542,7 +1666,7 @@ For cross-cutting modifiers (`.optional`/`.nullable`/`.default`/`.catch`/`.in`/`
|
|
|
1542
1666
|
- Don't pick `v.scalar` because "it's flexible". Flexibility at this layer usually means a missing discriminator — try `v.literal` or `v.union` first.
|
|
1543
1667
|
- Don't reach for `v.instanceof(Date)` when `v.date()` works. The latter is purpose-built.
|
|
1544
1668
|
- Don't use `v.string().oneOf(["a", "b"])` for discriminator fields where you need the literal type — use `v.literal("a", "b")`.
|
|
1545
|
-
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when
|
|
1669
|
+
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when _producing_ a value.
|
|
1546
1670
|
|
|
1547
1671
|
|
|
1548
1672
|
### pick-seal-primitive/boolean-methods.md `@warlock.js/seal/pick-seal-primitive/boolean-methods.md`
|
|
@@ -1661,139 +1785,139 @@ v.object({
|
|
|
1661
1785
|
|
|
1662
1786
|
## Range — global value comparison
|
|
1663
1787
|
|
|
1664
|
-
| Method
|
|
1665
|
-
|
|
1666
|
-
| `.min(dateOrField, msg?)`
|
|
1667
|
-
| `.max(dateOrField, msg?)`
|
|
1668
|
-
| `.before(dateOrField, msg?)` | strict `<`
|
|
1669
|
-
| `.after(dateOrField, msg?)`
|
|
1670
|
-
| `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)`
|
|
1788
|
+
| Method | Args | Example |
|
|
1789
|
+
| ---------------------------- | --------------- | ---------------------------------------------------------- |
|
|
1790
|
+
| `.min(dateOrField, msg?)` | inclusive `>=` | `v.date().min("2024-01-01")` or `v.date().min(new Date())` |
|
|
1791
|
+
| `.max(dateOrField, msg?)` | inclusive `<=` | `v.date().max(new Date())` |
|
|
1792
|
+
| `.before(dateOrField, msg?)` | strict `<` | `v.date().before(new Date())` |
|
|
1793
|
+
| `.after(dateOrField, msg?)` | strict `>` | `v.date().after(new Date())` |
|
|
1794
|
+
| `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)` |
|
|
1671
1795
|
|
|
1672
1796
|
Smart detection: a string with `-` or `/` is a date string; a plain string is a sibling field name.
|
|
1673
1797
|
|
|
1674
1798
|
## Range — explicit sibling scope
|
|
1675
1799
|
|
|
1676
|
-
| Method
|
|
1677
|
-
|
|
1678
|
-
| `.minSibling(field, msg?)`
|
|
1679
|
-
| `.maxSibling(field, msg?)`
|
|
1680
|
-
| `.beforeSibling(field, msg?)`
|
|
1681
|
-
| `.afterSibling(field, msg?)`
|
|
1682
|
-
| `.sameAsField(field, msg?)`
|
|
1800
|
+
| Method | Effect |
|
|
1801
|
+
| ---------------------------------- | ------------------------------------ |
|
|
1802
|
+
| `.minSibling(field, msg?)` | `>=` sibling field |
|
|
1803
|
+
| `.maxSibling(field, msg?)` | `<=` sibling field |
|
|
1804
|
+
| `.beforeSibling(field, msg?)` | `<` sibling field |
|
|
1805
|
+
| `.afterSibling(field, msg?)` | `>` sibling field |
|
|
1806
|
+
| `.sameAsField(field, msg?)` | `===` sibling field |
|
|
1683
1807
|
| `.sameAsFieldSibling(field, msg?)` | `===` sibling field (explicit scope) |
|
|
1684
1808
|
|
|
1685
1809
|
Only run inside `v.object`. Not representable in JSON Schema.
|
|
1686
1810
|
|
|
1687
1811
|
## Today / past / future
|
|
1688
1812
|
|
|
1689
|
-
| Method
|
|
1690
|
-
|
|
1691
|
-
| `.today(msg?)`
|
|
1692
|
-
| `.fromToday(msg?)`
|
|
1813
|
+
| Method | Effect |
|
|
1814
|
+
| -------------------- | --------------------- |
|
|
1815
|
+
| `.today(msg?)` | exactly today |
|
|
1816
|
+
| `.fromToday(msg?)` | today or future |
|
|
1693
1817
|
| `.beforeToday(msg?)` | strictly before today |
|
|
1694
|
-
| `.afterToday(msg?)`
|
|
1695
|
-
| `.past(msg?)`
|
|
1696
|
-
| `.future(msg?)`
|
|
1818
|
+
| `.afterToday(msg?)` | strictly after today |
|
|
1819
|
+
| `.past(msg?)` | any past date |
|
|
1820
|
+
| `.future(msg?)` | any future date |
|
|
1697
1821
|
|
|
1698
1822
|
## Relative window
|
|
1699
1823
|
|
|
1700
|
-
| Method
|
|
1701
|
-
|
|
1702
|
-
| `.withinDays(n, msg?)`
|
|
1703
|
-
| `.withinPastDays(n, msg?)`
|
|
1704
|
-
| `.withinFutureDays(n, msg?)` | within N days in the future
|
|
1824
|
+
| Method | Args | Effect |
|
|
1825
|
+
| ---------------------------- | ---------------------------- | ------ |
|
|
1826
|
+
| `.withinDays(n, msg?)` | within N days past or future | — |
|
|
1827
|
+
| `.withinPastDays(n, msg?)` | within N days in the past | — |
|
|
1828
|
+
| `.withinFutureDays(n, msg?)` | within N days in the future | — |
|
|
1705
1829
|
|
|
1706
1830
|
## Age
|
|
1707
1831
|
|
|
1708
|
-
| Method
|
|
1709
|
-
|
|
1710
|
-
| `.age(years, msg?)`
|
|
1711
|
-
| `.minAge(years, msg?)`
|
|
1712
|
-
| `.maxAge(years, msg?)`
|
|
1713
|
-
| `.betweenAge(min, max, msg?)`
|
|
1832
|
+
| Method | Args | Effect |
|
|
1833
|
+
| ----------------------------------- | --------------------------------- | ------ |
|
|
1834
|
+
| `.age(years, msg?)` | exactly N years old |
|
|
1835
|
+
| `.minAge(years, msg?)` | at least N years old |
|
|
1836
|
+
| `.maxAge(years, msg?)` | at most N years old |
|
|
1837
|
+
| `.betweenAge(min, max, msg?)` | between min/max years |
|
|
1714
1838
|
| `.birthday(minAge?, maxAge?, msg?)` | not in future, optional age range |
|
|
1715
1839
|
|
|
1716
1840
|
## Weekday / weekend / business day
|
|
1717
1841
|
|
|
1718
|
-
| Method
|
|
1719
|
-
|
|
1720
|
-
| `.weekDay(day, msg?)`
|
|
1721
|
-
| `.weekdays(days, msg?)` | array of weekdays
|
|
1722
|
-
| `.weekend(msg?)`
|
|
1723
|
-
| `.businessDay(msg?)`
|
|
1842
|
+
| Method | Args | Effect |
|
|
1843
|
+
| ----------------------- | ----------------------------- | ------ |
|
|
1844
|
+
| `.weekDay(day, msg?)` | day = `"monday"` … `"sunday"` |
|
|
1845
|
+
| `.weekdays(days, msg?)` | array of weekdays |
|
|
1846
|
+
| `.weekend(msg?)` | Saturday or Sunday |
|
|
1847
|
+
| `.businessDay(msg?)` | Monday – Friday |
|
|
1724
1848
|
|
|
1725
1849
|
## Period — month / year / quarter
|
|
1726
1850
|
|
|
1727
|
-
| Method
|
|
1728
|
-
|
|
1729
|
-
| `.month(m, msg?)`
|
|
1730
|
-
| `.year(y, msg?)`
|
|
1731
|
-
| `.quarter(q, msg?)`
|
|
1732
|
-
| `.leapYear(msg?)`
|
|
1733
|
-
| `.minYear(yearOrField, msg?)`
|
|
1734
|
-
| `.maxYear(yearOrField, msg?)`
|
|
1735
|
-
| `.minMonth(mOrField, msg?)`
|
|
1736
|
-
| `.maxMonth(mOrField, msg?)`
|
|
1737
|
-
| `.minDay(dOrField, msg?)`
|
|
1738
|
-
| `.maxDay(dOrField, msg?)`
|
|
1739
|
-
| `.betweenYears(start, end, msg?)`
|
|
1740
|
-
| `.betweenMonths(start, end, msg?)` | inclusive month range
|
|
1741
|
-
| `.betweenDays(start, end, msg?)`
|
|
1851
|
+
| Method | Args | Effect |
|
|
1852
|
+
| ---------------------------------- | ---------------------------- | ------ |
|
|
1853
|
+
| `.month(m, msg?)` | `m` = 1–12 (or `Month` enum) |
|
|
1854
|
+
| `.year(y, msg?)` | exact year |
|
|
1855
|
+
| `.quarter(q, msg?)` | `q` = 1–4 |
|
|
1856
|
+
| `.leapYear(msg?)` | year is a leap year |
|
|
1857
|
+
| `.minYear(yearOrField, msg?)` | year `>=` |
|
|
1858
|
+
| `.maxYear(yearOrField, msg?)` | year `<=` |
|
|
1859
|
+
| `.minMonth(mOrField, msg?)` | month `>=` |
|
|
1860
|
+
| `.maxMonth(mOrField, msg?)` | month `<=` |
|
|
1861
|
+
| `.minDay(dOrField, msg?)` | day-of-month `>=` |
|
|
1862
|
+
| `.maxDay(dOrField, msg?)` | day-of-month `<=` |
|
|
1863
|
+
| `.betweenYears(start, end, msg?)` | inclusive year range |
|
|
1864
|
+
| `.betweenMonths(start, end, msg?)` | inclusive month range |
|
|
1865
|
+
| `.betweenDays(start, end, msg?)` | inclusive day-of-month range |
|
|
1742
1866
|
|
|
1743
1867
|
Each `min*` / `max*` / `between*` accepts a sibling field name. Sibling-explicit variants exist: `.minYearSibling`, `.maxYearSibling`, `.minMonthSibling`, `.maxMonthSibling`, `.minDaySibling`, `.maxDaySibling`, `.betweenYearsSibling`, `.betweenMonthsSibling`, `.betweenDaysSibling`.
|
|
1744
1868
|
|
|
1745
1869
|
## Time — hour / minute
|
|
1746
1870
|
|
|
1747
|
-
| Method
|
|
1748
|
-
|
|
1749
|
-
| `.fromHour(h, msg?)`
|
|
1750
|
-
| `.beforeHour(h, msg?)`
|
|
1751
|
-
| `.betweenHours(start, end, msg?)`
|
|
1752
|
-
| `.fromMinute(m, msg?)`
|
|
1753
|
-
| `.beforeMinute(m, msg?)`
|
|
1754
|
-
| `.betweenMinutes(start, end, msg?)` | inclusive minute range
|
|
1755
|
-
| `.betweenTimes(start, end, msg?)`
|
|
1871
|
+
| Method | Args | Effect |
|
|
1872
|
+
| ----------------------------------- | ------------------------ | ------ |
|
|
1873
|
+
| `.fromHour(h, msg?)` | h = 0–23, time `>= h:00` |
|
|
1874
|
+
| `.beforeHour(h, msg?)` | time `< h:00` |
|
|
1875
|
+
| `.betweenHours(start, end, msg?)` | inclusive hour range |
|
|
1876
|
+
| `.fromMinute(m, msg?)` | m = 0–59 |
|
|
1877
|
+
| `.beforeMinute(m, msg?)` | — |
|
|
1878
|
+
| `.betweenMinutes(start, end, msg?)` | inclusive minute range |
|
|
1879
|
+
| `.betweenTimes(start, end, msg?)` | "HH:MM" strings |
|
|
1756
1880
|
|
|
1757
1881
|
## Format
|
|
1758
1882
|
|
|
1759
|
-
| Method
|
|
1760
|
-
|
|
1883
|
+
| Method | Args | Effect |
|
|
1884
|
+
| -------------------- | ------------------- | --------------------------- |
|
|
1761
1885
|
| `.format(fmt, msg?)` | dayjs format string | input must match the format |
|
|
1762
1886
|
|
|
1763
1887
|
## Mutators (pre-validation reshape)
|
|
1764
1888
|
|
|
1765
|
-
| Method
|
|
1766
|
-
|
|
1767
|
-
| `.toStartOfDay()`
|
|
1768
|
-
| `.toEndOfDay()`
|
|
1769
|
-
| `.toStartOfMonth()` | first day of month
|
|
1770
|
-
| `.toEndOfMonth()`
|
|
1771
|
-
| `.toStartOfYear()`
|
|
1772
|
-
| `.toEndOfYear()`
|
|
1773
|
-
| `.addDays(n)`
|
|
1774
|
-
| `.addMonths(n)`
|
|
1775
|
-
| `.addYears(n)`
|
|
1776
|
-
| `.addHours(n)`
|
|
1777
|
-
| `.toUTC()`
|
|
1778
|
-
|
|
1779
|
-
Mutators run
|
|
1889
|
+
| Method | Effect |
|
|
1890
|
+
| ------------------- | --------------------------------- |
|
|
1891
|
+
| `.toStartOfDay()` | 00:00:00.000 |
|
|
1892
|
+
| `.toEndOfDay()` | 23:59:59.999 |
|
|
1893
|
+
| `.toStartOfMonth()` | first day of month |
|
|
1894
|
+
| `.toEndOfMonth()` | last day of month |
|
|
1895
|
+
| `.toStartOfYear()` | January 1st |
|
|
1896
|
+
| `.toEndOfYear()` | December 31st |
|
|
1897
|
+
| `.addDays(n)` | shift by N days (negative = back) |
|
|
1898
|
+
| `.addMonths(n)` | shift by N months |
|
|
1899
|
+
| `.addYears(n)` | shift by N years |
|
|
1900
|
+
| `.addHours(n)` | shift by N hours |
|
|
1901
|
+
| `.toUTC()` | normalize to UTC |
|
|
1902
|
+
|
|
1903
|
+
Mutators run _before_ rules. `v.date().addDays(7).future()` checks whether the shifted date is in the future.
|
|
1780
1904
|
|
|
1781
1905
|
## Transformers (post-validation, reshape `data`)
|
|
1782
1906
|
|
|
1783
|
-
| Method
|
|
1784
|
-
|
|
1785
|
-
| `.toISOString()` | —
|
|
1786
|
-
| `.toTimestamp()` | —
|
|
1787
|
-
| `.toFormat(fmt)` | dayjs format string | `Date` → formatted string
|
|
1788
|
-
| `.toDateOnly()`
|
|
1789
|
-
| `.toTimeOnly()`
|
|
1907
|
+
| Method | Args | Effect |
|
|
1908
|
+
| ---------------- | ------------------- | ------------------------------------- |
|
|
1909
|
+
| `.toISOString()` | — | `Date` → `"2026-01-15T00:00:00.000Z"` |
|
|
1910
|
+
| `.toTimestamp()` | — | `Date` → number (ms since epoch) |
|
|
1911
|
+
| `.toFormat(fmt)` | dayjs format string | `Date` → formatted string |
|
|
1912
|
+
| `.toDateOnly()` | — | `"YYYY-MM-DD"` |
|
|
1913
|
+
| `.toTimeOnly()` | — | `"HH:mm:ss"` |
|
|
1790
1914
|
|
|
1791
|
-
Transformers shape the
|
|
1915
|
+
Transformers shape the _output_. `Infer<>` still resolves to `Date` even if a transformer changes the runtime shape — `Infer` reads the validator type, not the transformer pipeline.
|
|
1792
1916
|
|
|
1793
1917
|
## Defaults
|
|
1794
1918
|
|
|
1795
|
-
| Method
|
|
1796
|
-
|
|
1919
|
+
| Method | Effect |
|
|
1920
|
+
| --------------- | ------------------------------------------ |
|
|
1797
1921
|
| `.defaultNow()` | shorthand for `.default(() => new Date())` |
|
|
1798
1922
|
|
|
1799
1923
|
## JSON Schema mapping
|
|
@@ -1807,28 +1931,28 @@ Transformers shape the *output*. `Infer<>` still resolves to `Date` even if a tr
|
|
|
1807
1931
|
|
|
1808
1932
|
```ts
|
|
1809
1933
|
// Birthday — must be 13+ and in the past
|
|
1810
|
-
v.date().past().minAge(13)
|
|
1934
|
+
v.date().past().minAge(13);
|
|
1811
1935
|
|
|
1812
1936
|
// Reservation — future, business day, between hours
|
|
1813
|
-
v.date().future().businessDay().betweenHours(9, 17)
|
|
1937
|
+
v.date().future().businessDay().betweenHours(9, 17);
|
|
1814
1938
|
|
|
1815
1939
|
// Effective date range (cross-field)
|
|
1816
1940
|
v.object({
|
|
1817
1941
|
startsAt: v.date(),
|
|
1818
1942
|
endsAt: v.date().afterSibling("startsAt"),
|
|
1819
|
-
})
|
|
1943
|
+
});
|
|
1820
1944
|
|
|
1821
1945
|
// API response — emit ISO string
|
|
1822
|
-
v.date().toISOString()
|
|
1946
|
+
v.date().toISOString();
|
|
1823
1947
|
|
|
1824
1948
|
// Event window — within 30 future days, normalized to UTC
|
|
1825
|
-
v.date().toUTC().withinFutureDays(30)
|
|
1949
|
+
v.date().toUTC().withinFutureDays(30);
|
|
1826
1950
|
|
|
1827
1951
|
// Quarterly report
|
|
1828
|
-
v.date().quarter(1).year(2026)
|
|
1952
|
+
v.date().quarter(1).year(2026);
|
|
1829
1953
|
|
|
1830
1954
|
// Date-only, end-of-day for inclusive comparisons
|
|
1831
|
-
v.date().toEndOfDay().toDateOnly()
|
|
1955
|
+
v.date().toEndOfDay().toDateOnly();
|
|
1832
1956
|
```
|
|
1833
1957
|
|
|
1834
1958
|
|
|
@@ -1840,23 +1964,23 @@ All four share this surface. The picking guide (which factory to call) is in the
|
|
|
1840
1964
|
|
|
1841
1965
|
## Range — global value comparison
|
|
1842
1966
|
|
|
1843
|
-
| Method
|
|
1844
|
-
|
|
1845
|
-
| `.min(n, msg?)`
|
|
1846
|
-
| `.max(n, msg?)`
|
|
1847
|
-
| `.between(a, b, msg?)`
|
|
1848
|
-
| `.greaterThan(n, msg?)` | strict `>`
|
|
1849
|
-
| `.gt(n, msg?)`
|
|
1850
|
-
| `.lessThan(n, msg?)`
|
|
1851
|
-
| `.lt(n, msg?)`
|
|
1967
|
+
| Method | Args | JSON Schema | Example |
|
|
1968
|
+
| ----------------------- | ------------------------ | ------------------------ | -------------------------- |
|
|
1969
|
+
| `.min(n, msg?)` | inclusive lower bound | `minimum: n` | `v.int().min(0)` |
|
|
1970
|
+
| `.max(n, msg?)` | inclusive upper bound | `maximum: n` | `v.int().max(100)` |
|
|
1971
|
+
| `.between(a, b, msg?)` | inclusive range | `minimum: a, maximum: b` | `v.number().between(0, 1)` |
|
|
1972
|
+
| `.greaterThan(n, msg?)` | strict `>` | `exclusiveMinimum: n` | `v.int().greaterThan(0)` |
|
|
1973
|
+
| `.gt(n, msg?)` | alias for `.greaterThan` | `exclusiveMinimum: n` | — |
|
|
1974
|
+
| `.lessThan(n, msg?)` | strict `<` | `exclusiveMaximum: n` | `v.int().lessThan(100)` |
|
|
1975
|
+
| `.lt(n, msg?)` | alias for `.lessThan` | `exclusiveMaximum: n` | — |
|
|
1852
1976
|
|
|
1853
1977
|
`.min` / `.max` / `.between` / `.greaterThan` / `.lessThan` accept a **string** as the value — interpreted as a sibling field name (smart detection):
|
|
1854
1978
|
|
|
1855
1979
|
```ts
|
|
1856
1980
|
v.object({
|
|
1857
1981
|
minPrice: v.int(),
|
|
1858
|
-
maxPrice: v.int().min("minPrice"),
|
|
1859
|
-
})
|
|
1982
|
+
maxPrice: v.int().min("minPrice"), // maxPrice >= minPrice
|
|
1983
|
+
});
|
|
1860
1984
|
```
|
|
1861
1985
|
|
|
1862
1986
|
Sibling references are **not representable in JSON Schema** — silently omitted from generated output.
|
|
@@ -1865,68 +1989,68 @@ Sibling references are **not representable in JSON Schema** — silently omitted
|
|
|
1865
1989
|
|
|
1866
1990
|
For when smart detection is ambiguous (e.g. a numeric string that happens to match a field name):
|
|
1867
1991
|
|
|
1868
|
-
| Method
|
|
1869
|
-
|
|
1870
|
-
| `.minSibling(field, msg?)`
|
|
1871
|
-
| `.maxSibling(field, msg?)`
|
|
1872
|
-
| `.greaterThanSibling(field, msg?)`
|
|
1873
|
-
| `.gtSibling(field, msg?)`
|
|
1874
|
-
| `.lessThanSibling(field, msg?)`
|
|
1875
|
-
| `.ltSibling(field, msg?)`
|
|
1992
|
+
| Method | Effect |
|
|
1993
|
+
| ------------------------------------------- | -------------------------- |
|
|
1994
|
+
| `.minSibling(field, msg?)` | value `>=` sibling field |
|
|
1995
|
+
| `.maxSibling(field, msg?)` | value `<=` sibling field |
|
|
1996
|
+
| `.greaterThanSibling(field, msg?)` | value `>` sibling field |
|
|
1997
|
+
| `.gtSibling(field, msg?)` | alias |
|
|
1998
|
+
| `.lessThanSibling(field, msg?)` | value `<` sibling field |
|
|
1999
|
+
| `.ltSibling(field, msg?)` | alias |
|
|
1876
2000
|
| `.betweenSibling(minField, maxField, msg?)` | between two sibling fields |
|
|
1877
2001
|
|
|
1878
2002
|
These **only run inside `v.object`** — sibling resolution silently passes otherwise.
|
|
1879
2003
|
|
|
1880
2004
|
## Sign & parity
|
|
1881
2005
|
|
|
1882
|
-
| Method
|
|
1883
|
-
|
|
1884
|
-
| `.positive(msg?)` | value `> 0`
|
|
1885
|
-
| `.negative(msg?)` | value `< 0`
|
|
1886
|
-
| `.odd(msg?)`
|
|
1887
|
-
| `.even(msg?)`
|
|
2006
|
+
| Method | Effect | Example |
|
|
2007
|
+
| ----------------- | ------------- | -------------------- |
|
|
2008
|
+
| `.positive(msg?)` | value `> 0` | `v.int().positive()` |
|
|
2009
|
+
| `.negative(msg?)` | value `< 0` | `v.int().negative()` |
|
|
2010
|
+
| `.odd(msg?)` | value is odd | `v.int().odd()` |
|
|
2011
|
+
| `.even(msg?)` | value is even | `v.int().even()` |
|
|
1888
2012
|
|
|
1889
2013
|
## Divisibility / modulo
|
|
1890
2014
|
|
|
1891
|
-
| Method
|
|
1892
|
-
|
|
1893
|
-
| `.modulo(n, msg?)`
|
|
1894
|
-
| `.divisibleBy(n, msg?)` | alias
|
|
1895
|
-
| `.multipleOf(n, msg?)`
|
|
1896
|
-
| `.modulusOf(n, msg?)`
|
|
2015
|
+
| Method | Effect | JSON Schema |
|
|
2016
|
+
| ----------------------- | ----------------- | --------------- |
|
|
2017
|
+
| `.modulo(n, msg?)` | value `% n === 0` | `multipleOf: n` |
|
|
2018
|
+
| `.divisibleBy(n, msg?)` | alias | `multipleOf: n` |
|
|
2019
|
+
| `.multipleOf(n, msg?)` | alias | `multipleOf: n` |
|
|
2020
|
+
| `.modulusOf(n, msg?)` | alias | `multipleOf: n` |
|
|
1897
2021
|
|
|
1898
2022
|
## String-form length (rare)
|
|
1899
2023
|
|
|
1900
2024
|
When the numeric value is a fixed-format code (PINs, IDs):
|
|
1901
2025
|
|
|
1902
|
-
| Method
|
|
1903
|
-
|
|
1904
|
-
| `.length(n, msg?)`
|
|
1905
|
-
| `.minLength(n, msg?)` | string-rep length `>= n`
|
|
1906
|
-
| `.maxLength(n, msg?)` | string-rep length `<= n`
|
|
2026
|
+
| Method | Effect |
|
|
2027
|
+
| --------------------- | ------------------------------------- |
|
|
2028
|
+
| `.length(n, msg?)` | string-rep length must be exactly `n` |
|
|
2029
|
+
| `.minLength(n, msg?)` | string-rep length `>= n` |
|
|
2030
|
+
| `.maxLength(n, msg?)` | string-rep length `<= n` |
|
|
1907
2031
|
|
|
1908
2032
|
For most numeric-shaped IDs you'd use `v.string().length(n).numeric()` instead — let the value be a string.
|
|
1909
2033
|
|
|
1910
2034
|
## Mutators (pre-validation reshape)
|
|
1911
2035
|
|
|
1912
|
-
| Method
|
|
1913
|
-
|
|
1914
|
-
| `.abs()`
|
|
1915
|
-
| `.ceil()`
|
|
1916
|
-
| `.floor()`
|
|
1917
|
-
| `.round(decimals?)`
|
|
1918
|
-
| `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number**
|
|
1919
|
-
| `.coerce()`
|
|
2036
|
+
| Method | Args | Effect |
|
|
2037
|
+
| --------------------- | --------- | -------------------------------------------------------------------------------- |
|
|
2038
|
+
| `.abs()` | — | `Math.abs(value)` |
|
|
2039
|
+
| `.ceil()` | — | round up to integer |
|
|
2040
|
+
| `.floor()` | — | round down to integer |
|
|
2041
|
+
| `.round(decimals?)` | default 0 | round to N decimals |
|
|
2042
|
+
| `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number** |
|
|
2043
|
+
| `.coerce()` | — | a **numeric-shaped string** → `Number`; any other value passes through unchanged |
|
|
1920
2044
|
|
|
1921
|
-
These run
|
|
2045
|
+
These run _before_ validation rules. If you mutate `1.6` with `.ceil()`, `v.int()` sees `2` and passes. Use mutators when the input arrives in a slightly wrong form and you want to coerce, not reject.
|
|
1922
2046
|
|
|
1923
2047
|
`.coerce()` (5.9+) is the opt-in for the most common case: **query-string params arrive as strings.** `v.int()` is strict by default — `?limit=2` is the string `"2"` and is rejected — so add `.coerce()` when a schema validates `request.query`:
|
|
1924
2048
|
|
|
1925
2049
|
```ts
|
|
1926
|
-
v.object({ limit: v.int().coerce().min(1).max(100) })
|
|
2050
|
+
v.object({ limit: v.int().coerce().min(1).max(100) }); // "2" → 2, passes
|
|
1927
2051
|
```
|
|
1928
2052
|
|
|
1929
|
-
It only fixes the string
|
|
2053
|
+
It only fixes the string _representation_ — the type rule still runs, so `v.int().coerce()` on `"5.5"` coerces to `5.5` and correctly **fails** (still not an integer), and `"abc"` passes through and fails. Strictness is unchanged unless you chain `.coerce()`; the output type is unaffected (`v.int().coerce()` still infers `number`). Prefer this over hand-rolling `.addMutator(...)` in app schemas.
|
|
1930
2054
|
|
|
1931
2055
|
That ordering is exactly why `.toFixed()` yields a number rather than a fixed-point string: the mutated value is what the validator's own type rule sees, and a `number` schema that emitted `"3.14"` would reject its own output. Format to a string at the presentation edge instead.
|
|
1932
2056
|
|
|
@@ -1946,31 +2070,31 @@ Before 4.9.2 `.toFixed()` returned a string, so `v.number().toFixed(2)` **always
|
|
|
1946
2070
|
|
|
1947
2071
|
```ts
|
|
1948
2072
|
// Age
|
|
1949
|
-
v.int().min(0).max(150)
|
|
2073
|
+
v.int().min(0).max(150);
|
|
1950
2074
|
|
|
1951
2075
|
// Price (cents)
|
|
1952
|
-
v.int().min(0)
|
|
2076
|
+
v.int().min(0);
|
|
1953
2077
|
|
|
1954
2078
|
// Probability
|
|
1955
|
-
v.number().between(0, 1)
|
|
2079
|
+
v.number().between(0, 1);
|
|
1956
2080
|
|
|
1957
2081
|
// Even page index
|
|
1958
|
-
v.int().min(0).even()
|
|
2082
|
+
v.int().min(0).even();
|
|
1959
2083
|
|
|
1960
2084
|
// Quantity divisible by box size
|
|
1961
|
-
v.int().min(1).multipleOf(12)
|
|
2085
|
+
v.int().min(1).multipleOf(12);
|
|
1962
2086
|
|
|
1963
2087
|
// Coerced from form input
|
|
1964
|
-
v.numeric().min(0).max(100)
|
|
2088
|
+
v.numeric().min(0).max(100);
|
|
1965
2089
|
|
|
1966
2090
|
// Cross-field range
|
|
1967
2091
|
v.object({
|
|
1968
2092
|
startYear: v.int(),
|
|
1969
2093
|
endYear: v.int().minSibling("startYear"),
|
|
1970
|
-
})
|
|
2094
|
+
});
|
|
1971
2095
|
|
|
1972
2096
|
// Optional with default
|
|
1973
|
-
v.int().min(0).default(0).optional()
|
|
2097
|
+
v.int().min(0).default(0).optional();
|
|
1974
2098
|
```
|
|
1975
2099
|
|
|
1976
2100
|
|
|
@@ -1984,168 +2108,173 @@ For the picking guide (`v.string` vs `v.scalar` vs `v.literal`), see the `pick-s
|
|
|
1984
2108
|
|
|
1985
2109
|
## Length
|
|
1986
2110
|
|
|
1987
|
-
| Method
|
|
1988
|
-
|
|
1989
|
-
| `.min(n, msg?)`
|
|
1990
|
-
| `.minLength(n, msg?)`
|
|
1991
|
-
| `.max(n, msg?)`
|
|
1992
|
-
| `.maxLength(n, msg?)`
|
|
1993
|
-
| `.length(n, msg?)`
|
|
1994
|
-
| `.lengthBetween(a, b, msg?)` | min and max length
|
|
2111
|
+
| Method | Args | JSON Schema | Example |
|
|
2112
|
+
| ---------------------------- | --------------------- | ---------------------------- | --------------------------------- |
|
|
2113
|
+
| `.min(n, msg?)` | min length, inclusive | `minLength: n` | `v.string().min(3)` |
|
|
2114
|
+
| `.minLength(n, msg?)` | alias for `.min()` | `minLength: n` | `v.string().minLength(3)` |
|
|
2115
|
+
| `.max(n, msg?)` | max length, inclusive | `maxLength: n` | `v.string().max(120)` |
|
|
2116
|
+
| `.maxLength(n, msg?)` | alias for `.max()` | `maxLength: n` | `v.string().maxLength(120)` |
|
|
2117
|
+
| `.length(n, msg?)` | exact length | `minLength=maxLength=n` | `v.string().length(10)` |
|
|
2118
|
+
| `.lengthBetween(a, b, msg?)` | min and max length | `minLength: a, maxLength: b` | `v.string().lengthBetween(5, 30)` |
|
|
1995
2119
|
|
|
1996
2120
|
## Format
|
|
1997
2121
|
|
|
1998
|
-
| Method
|
|
1999
|
-
|
|
2000
|
-
| `.email(msg?)`
|
|
2001
|
-
| `.url(msg?)`
|
|
2002
|
-
| `.pattern(re, msg?)`
|
|
2003
|
-
| `.alpha(msg?)`
|
|
2004
|
-
| `.alphanumeric(msg?)`
|
|
2005
|
-
| `.numeric(msg?)`
|
|
2006
|
-
| `.withoutWhitespace(msg?)`
|
|
2007
|
-
| `.creditCard(msg?)`
|
|
2008
|
-
| `.ip(msg?)`
|
|
2009
|
-
| `.ip4(msg?)`
|
|
2010
|
-
| `.ip6(msg?)`
|
|
2011
|
-
| `.strongPassword(minLen?, msg?)` | 8+ chars, upper+lower+digit+symbol | —
|
|
2122
|
+
| Method | Pattern | JSON Schema | Example |
|
|
2123
|
+
| -------------------------------- | ---------------------------------- | -------------------- | -------------------------------- |
|
|
2124
|
+
| `.email(msg?)` | RFC-flavored regex | `format: "email"` | `v.string().email()` |
|
|
2125
|
+
| `.url(msg?)` | http/https URL | `format: "uri"` | `v.string().url()` |
|
|
2126
|
+
| `.pattern(re, msg?)` | custom regex | `pattern: re.source` | `v.string().pattern(/^[A-Z]/)` |
|
|
2127
|
+
| `.alpha(msg?)` | letters only | — | `v.string().alpha()` |
|
|
2128
|
+
| `.alphanumeric(msg?)` | letters + digits | — | `v.string().alphanumeric()` |
|
|
2129
|
+
| `.numeric(msg?)` | digits only (string) | — | `v.string().numeric()` |
|
|
2130
|
+
| `.withoutWhitespace(msg?)` | no spaces/tabs/newlines | — | `v.string().withoutWhitespace()` |
|
|
2131
|
+
| `.creditCard(msg?)` | credit-card-shaped | — | `v.string().creditCard()` |
|
|
2132
|
+
| `.ip(msg?)` | IPv4 or IPv6 | `format: "ipv4"` | `v.string().ip()` |
|
|
2133
|
+
| `.ip4(msg?)` | IPv4 only | `format: "ipv4"` | `v.string().ip4()` |
|
|
2134
|
+
| `.ip6(msg?)` | IPv6 only | `format: "ipv6"` | `v.string().ip6()` |
|
|
2135
|
+
| `.strongPassword(minLen?, msg?)` | 8+ chars, upper+lower+digit+symbol | — | `v.string().strongPassword(12)` |
|
|
2012
2136
|
|
|
2013
2137
|
## ID formats
|
|
2014
2138
|
|
|
2015
|
-
| Method
|
|
2016
|
-
|
|
2017
|
-
| `.uuid(version?, msg?)`
|
|
2018
|
-
| `.cuid({ version?: 1\|2 }?)` | CUID2 default (24 chars, lowercase); v1 legacy | `pattern: …`
|
|
2019
|
-
| `.ulid(msg?)`
|
|
2020
|
-
| `.nanoid(length?, msg?)`
|
|
2139
|
+
| Method | Args | JSON Schema | Example |
|
|
2140
|
+
| ---------------------------- | ---------------------------------------------- | ---------------- | ----------------------- |
|
|
2141
|
+
| `.uuid(version?, msg?)` | UUID, any version or restrict to 1/3/4/5/6/7 | `format: "uuid"` | `v.string().uuid(4)` |
|
|
2142
|
+
| `.cuid({ version?: 1\|2 }?)` | CUID2 default (24 chars, lowercase); v1 legacy | `pattern: …` | `v.string().cuid()` |
|
|
2143
|
+
| `.ulid(msg?)` | 26 chars, Crockford base32 (no I/L/O/U) | `pattern: …` | `v.string().ulid()` |
|
|
2144
|
+
| `.nanoid(length?, msg?)` | URL-safe alphabet, default length 21 | `pattern: …` | `v.string().nanoid(21)` |
|
|
2021
2145
|
|
|
2022
2146
|
UUID validation is RFC 4122 strict — the variant nibble (8/9/a/b at position 17) is checked, so "looks-like-UUID-but-not-valid" inputs are rejected. CUID defaults to **CUID2** since CUID1 is deprecated by its original author; pass `{ version: 1 }` only for legacy data. nanoid's alphabet is fixed (`A-Za-z0-9_-`) — for custom alphabets use `.pattern()` directly.
|
|
2023
2147
|
|
|
2024
2148
|
## Word count
|
|
2025
2149
|
|
|
2026
|
-
| Method
|
|
2027
|
-
|
|
2028
|
-
| `.words(n, msg?)`
|
|
2029
|
-
| `.minWords(n, msg?)` | min words
|
|
2030
|
-
| `.maxWords(n, msg?)` | max words
|
|
2150
|
+
| Method | Args | Example |
|
|
2151
|
+
| -------------------- | ---------------- | ------------------------- |
|
|
2152
|
+
| `.words(n, msg?)` | exact word count | `v.string().words(5)` |
|
|
2153
|
+
| `.minWords(n, msg?)` | min words | `v.string().minWords(3)` |
|
|
2154
|
+
| `.maxWords(n, msg?)` | max words | `v.string().maxWords(50)` |
|
|
2031
2155
|
|
|
2032
2156
|
## Content
|
|
2033
2157
|
|
|
2034
|
-
| Method
|
|
2035
|
-
|
|
2036
|
-
| `.startsWith(s, msg?)`
|
|
2037
|
-
| `.endsWith(s, msg?)`
|
|
2038
|
-
| `.contains(s, msg?)`
|
|
2158
|
+
| Method | Args | Example |
|
|
2159
|
+
| ----------------------- | ----------------- | --------------------------------------- |
|
|
2160
|
+
| `.startsWith(s, msg?)` | prefix check | `v.string().startsWith("https://")` |
|
|
2161
|
+
| `.endsWith(s, msg?)` | suffix check | `v.string().endsWith(".pdf")` |
|
|
2162
|
+
| `.contains(s, msg?)` | substring check | `v.string().contains("@")` |
|
|
2039
2163
|
| `.notContains(s, msg?)` | inverse substring | `v.string().notContains("javascript:")` |
|
|
2040
2164
|
|
|
2041
2165
|
## Color
|
|
2042
2166
|
|
|
2043
|
-
| Method
|
|
2044
|
-
|
|
2045
|
-
| `.color(msg?)`
|
|
2046
|
-
| `.hexColor(msg?)`
|
|
2047
|
-
| `.rgbColor(msg?)`
|
|
2048
|
-
| `.rgbaColor(msg?)`
|
|
2049
|
-
| `.hslColor(msg?)`
|
|
2050
|
-
| `.lightColor(msg?)` | luminance-based
|
|
2051
|
-
| `.darkColor(msg?)`
|
|
2167
|
+
| Method | Accepts | Example |
|
|
2168
|
+
| ------------------- | ------------------- | ----------------------- |
|
|
2169
|
+
| `.color(msg?)` | any valid CSS color | `v.string().color()` |
|
|
2170
|
+
| `.hexColor(msg?)` | `#rgb`, `#rrggbb` | `v.string().hexColor()` |
|
|
2171
|
+
| `.rgbColor(msg?)` | `rgb(r,g,b)` | — |
|
|
2172
|
+
| `.rgbaColor(msg?)` | `rgba(r,g,b,a)` | — |
|
|
2173
|
+
| `.hslColor(msg?)` | `hsl(h,s,l)` | — |
|
|
2174
|
+
| `.lightColor(msg?)` | luminance-based | — |
|
|
2175
|
+
| `.darkColor(msg?)` | luminance-based | — |
|
|
2052
2176
|
|
|
2053
2177
|
JSON Schema: `format: "color"` for `.hexColor()`; the others map to `format: "color"` only via `.hexColor()` — for OpenAPI consumers expecting strict format, prefer `.hexColor()`.
|
|
2054
2178
|
|
|
2055
2179
|
## Case mutators (pre-validation)
|
|
2056
2180
|
|
|
2057
|
-
| Method
|
|
2058
|
-
|
|
2059
|
-
| `.uppercase()`
|
|
2060
|
-
| `.lowercase()`
|
|
2061
|
-
| `.capitalize()` | "hello world" → "Hello world" | —
|
|
2062
|
-
| `.titleCase()`
|
|
2063
|
-
| `.camelCase()`
|
|
2064
|
-
| `.pascalCase()` | "hello world" → "HelloWorld"
|
|
2065
|
-
| `.snakeCase()`
|
|
2066
|
-
| `.kebabCase()`
|
|
2181
|
+
| Method | Effect | Example |
|
|
2182
|
+
| --------------- | ----------------------------- | ------------------------ |
|
|
2183
|
+
| `.uppercase()` | "Hello" → "HELLO" | `v.string().uppercase()` |
|
|
2184
|
+
| `.lowercase()` | "Hello" → "hello" | — |
|
|
2185
|
+
| `.capitalize()` | "hello world" → "Hello world" | — |
|
|
2186
|
+
| `.titleCase()` | "hello world" → "Hello World" | — |
|
|
2187
|
+
| `.camelCase()` | "hello world" → "helloWorld" | — |
|
|
2188
|
+
| `.pascalCase()` | "hello world" → "HelloWorld" | — |
|
|
2189
|
+
| `.snakeCase()` | "hello world" → "hello_world" | — |
|
|
2190
|
+
| `.kebabCase()` | "hello world" → "hello-world" | — |
|
|
2067
2191
|
|
|
2068
2192
|
## Trim & whitespace mutators
|
|
2069
2193
|
|
|
2070
|
-
| Method
|
|
2071
|
-
|
|
2072
|
-
| `.trim(needle?)`
|
|
2073
|
-
| `.ltrim(needle?)`
|
|
2074
|
-
| `.rtrim(needle?)`
|
|
2075
|
-
| `.trimMultipleWhitespace()` | —
|
|
2076
|
-
| `.padStart(len, char?)`
|
|
2077
|
-
| `.padEnd(len, char?)`
|
|
2194
|
+
| Method | Args | Effect |
|
|
2195
|
+
| --------------------------- | ------------------ | ------------------- |
|
|
2196
|
+
| `.trim(needle?)` | default = space | trim both ends |
|
|
2197
|
+
| `.ltrim(needle?)` | — | trim left only |
|
|
2198
|
+
| `.rtrim(needle?)` | — | trim right only |
|
|
2199
|
+
| `.trimMultipleWhitespace()` | — | "a b" → "a b" |
|
|
2200
|
+
| `.padStart(len, char?)` | char default = " " | left-pad to length |
|
|
2201
|
+
| `.padEnd(len, char?)` | char default = " " | right-pad to length |
|
|
2078
2202
|
|
|
2079
2203
|
## Replace, append, modify mutators
|
|
2080
2204
|
|
|
2081
|
-
| Method
|
|
2082
|
-
|
|
2083
|
-
| `.replace(search, replace)`
|
|
2084
|
-
| `.replaceAll(search, replace)` | string or RegExp + string | —
|
|
2085
|
-
| `.append(suffix)`
|
|
2086
|
-
| `.prepend(prefix)`
|
|
2087
|
-
| `.reverse()`
|
|
2088
|
-
| `.repeat(count)`
|
|
2089
|
-
| `.truncate(maxLen, suffix?)`
|
|
2090
|
-
| `.mask(start, end?, char?)`
|
|
2205
|
+
| Method | Args | Example |
|
|
2206
|
+
| ------------------------------ | ------------------------- | -------------------------------------- |
|
|
2207
|
+
| `.replace(search, replace)` | string or RegExp + string | — |
|
|
2208
|
+
| `.replaceAll(search, replace)` | string or RegExp + string | — |
|
|
2209
|
+
| `.append(suffix)` | string | "foo" → "foobar" via `.append("bar")` |
|
|
2210
|
+
| `.prepend(prefix)` | string | "foo" → "barfoo" via `.prepend("bar")` |
|
|
2211
|
+
| `.reverse()` | — | "abc" → "cba" |
|
|
2212
|
+
| `.repeat(count)` | number | "ab" → "ababab" via `.repeat(3)` |
|
|
2213
|
+
| `.truncate(maxLen, suffix?)` | suffix default = "…" | "long text" → "long…" |
|
|
2214
|
+
| `.mask(start, end?, char?)` | char default = "*" | "1234567890" → "12******90" |
|
|
2091
2215
|
|
|
2092
2216
|
## Filter mutators
|
|
2093
2217
|
|
|
2094
|
-
| Method
|
|
2095
|
-
|
|
2096
|
-
| `.toAlpha()`
|
|
2097
|
-
| `.toAlphanumeric()`
|
|
2098
|
-
| `.removeSpecialCharacters()` | keep alphanumerics + whitespace
|
|
2099
|
-
| `.removeNumbers()`
|
|
2100
|
-
| `.stripTags()`
|
|
2101
|
-
| `.htmlEscape()`
|
|
2102
|
-
| `.unescapeHtml()`
|
|
2218
|
+
| Method | Effect |
|
|
2219
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
2220
|
+
| `.toAlpha()` | strip non-letters |
|
|
2221
|
+
| `.toAlphanumeric()` | strip non-alphanumerics |
|
|
2222
|
+
| `.removeSpecialCharacters()` | keep alphanumerics + whitespace |
|
|
2223
|
+
| `.removeNumbers()` | strip digits |
|
|
2224
|
+
| `.stripTags()` | strip HTML tags via a naive `<...>` regex — **not** an XSS sanitizer; `.safeHtml()` is a deprecated alias |
|
|
2225
|
+
| `.htmlEscape()` | `<` → `<` etc |
|
|
2226
|
+
| `.unescapeHtml()` | reverse `htmlEscape()` |
|
|
2103
2227
|
|
|
2104
2228
|
## Encoding mutators
|
|
2105
2229
|
|
|
2106
|
-
| Method
|
|
2107
|
-
|
|
2108
|
-
| `.base64Encode()` | utf8 → base64
|
|
2109
|
-
| `.base64Decode()` | base64 → utf8
|
|
2110
|
-
| `.urlEncode()`
|
|
2111
|
-
| `.urlDecode()`
|
|
2112
|
-
| `.slug()`
|
|
2113
|
-
| `.toString()`
|
|
2230
|
+
| Method | Effect |
|
|
2231
|
+
| ----------------- | --------------------------------- |
|
|
2232
|
+
| `.base64Encode()` | utf8 → base64 |
|
|
2233
|
+
| `.base64Decode()` | base64 → utf8 |
|
|
2234
|
+
| `.urlEncode()` | percent-encode |
|
|
2235
|
+
| `.urlDecode()` | percent-decode |
|
|
2236
|
+
| `.slug()` | "Hello World!" → "hello-world" |
|
|
2237
|
+
| `.toString()` | coerce non-string input to string |
|
|
2114
2238
|
|
|
2115
2239
|
## Mutator vs transformer
|
|
2116
2240
|
|
|
2117
|
-
The methods above are **all mutators** — they reshape the value
|
|
2241
|
+
The methods above are **all mutators** — they reshape the value _before_ validation rules run. If you want post-validation reshaping, attach via `.addTransformer(fn)` (see the `compose-seal-modifiers` topic).
|
|
2118
2242
|
|
|
2119
|
-
Practical implication: `v.string().min(3).trim()` runs `min(3)` against the
|
|
2243
|
+
Practical implication: `v.string().min(3).trim()` runs `min(3)` against the _un-trimmed_ input. To check trimmed length, mutate first: `v.string().trim().min(3)`.
|
|
2120
2244
|
|
|
2121
2245
|
## Common chains
|
|
2122
2246
|
|
|
2123
2247
|
```ts
|
|
2124
2248
|
// Email field
|
|
2125
|
-
v.string().email()
|
|
2249
|
+
v.string().email();
|
|
2126
2250
|
|
|
2127
2251
|
// Username
|
|
2128
|
-
v.string().min(3).max(30).alphanumeric().lowercase()
|
|
2252
|
+
v.string().min(3).max(30).alphanumeric().lowercase();
|
|
2129
2253
|
|
|
2130
2254
|
// Slug from title
|
|
2131
|
-
v.string().slug()
|
|
2255
|
+
v.string().slug();
|
|
2132
2256
|
|
|
2133
2257
|
// Strong password
|
|
2134
|
-
v.string().strongPassword(12)
|
|
2258
|
+
v.string().strongPassword(12);
|
|
2135
2259
|
|
|
2136
2260
|
// URL with strict format
|
|
2137
|
-
v.string().url().startsWith("https://")
|
|
2261
|
+
v.string().url().startsWith("https://");
|
|
2138
2262
|
|
|
2139
2263
|
// Tag-stripped HTML body (not XSS-safe — sanitize with DOMPurify/sanitize-html before rendering untrusted rich text)
|
|
2140
|
-
v.string().stripTags().min(1)
|
|
2264
|
+
v.string().stripTags().min(1);
|
|
2141
2265
|
|
|
2142
2266
|
// Masked phone for response
|
|
2143
|
-
v.string()
|
|
2267
|
+
v.string()
|
|
2268
|
+
.pattern(/^\+\d{8,}$/)
|
|
2269
|
+
.mask(3, -2);
|
|
2144
2270
|
|
|
2145
2271
|
// Optional with default
|
|
2146
|
-
v.string().email().default("guest@example.com").optional()
|
|
2272
|
+
v.string().email().default("guest@example.com").optional();
|
|
2147
2273
|
```
|
|
2148
2274
|
|
|
2275
|
+
The standalone `replaceMutator` and `replaceAllMutator` preserve the stringified
|
|
2276
|
+
value when called without a context or replacement options.
|
|
2277
|
+
|
|
2149
2278
|
|
|
2150
2279
|
## seal-basics `@warlock.js/seal/seal-basics/SKILL.md`
|
|
2151
2280
|
|
|
@@ -2156,7 +2285,7 @@ description: "Start here for @warlock.js/seal schemas, validation, parsing, and
|
|
|
2156
2285
|
|
|
2157
2286
|
# Validate with seal
|
|
2158
2287
|
|
|
2159
|
-
Schema-first validation. Single entry point: the `v` factory. Every validator chains, composes, and infers. Schemas double as runtime validators
|
|
2288
|
+
Schema-first validation. Single entry point: the `v` factory. Every validator chains, composes, and infers. Schemas double as runtime validators _and_ type-level shapes via `Infer<typeof schema>`, and ship `~standard` so any Standard-Schema-aware consumer accepts them directly.
|
|
2160
2289
|
|
|
2161
2290
|
> This skill is the seal **map** — read it first, then load the specific skill for the task.
|
|
2162
2291
|
|
|
@@ -2184,9 +2313,9 @@ type User = Infer<typeof userSchema>;
|
|
|
2184
2313
|
|
|
2185
2314
|
const result = await validate(userSchema, input);
|
|
2186
2315
|
if (result.isValid) {
|
|
2187
|
-
result.data;
|
|
2316
|
+
result.data; // typed, post-transformer
|
|
2188
2317
|
} else {
|
|
2189
|
-
result.errors;
|
|
2318
|
+
result.errors; // [{ type, error, input }, ...]
|
|
2190
2319
|
}
|
|
2191
2320
|
```
|
|
2192
2321
|
|
|
@@ -2242,13 +2371,13 @@ const schema = v.object({
|
|
|
2242
2371
|
status: v.enum(Status).optional().default(Status.ACTIVE),
|
|
2243
2372
|
});
|
|
2244
2373
|
|
|
2245
|
-
type In
|
|
2374
|
+
type In = Infer.Input<typeof schema>;
|
|
2246
2375
|
// { email?: string; status?: Status } ← caller may omit both
|
|
2247
2376
|
|
|
2248
2377
|
type Out = Infer.Output<typeof schema>;
|
|
2249
2378
|
// { email?: string; status: Status } ← default fired for status
|
|
2250
2379
|
|
|
2251
|
-
type Default = Infer<typeof schema>;
|
|
2380
|
+
type Default = Infer<typeof schema>; // alias for Infer.Input
|
|
2252
2381
|
```
|
|
2253
2382
|
|
|
2254
2383
|
Both flavours widen with `| null` when `.nullable()` is set.
|
|
@@ -2260,15 +2389,15 @@ Both flavours widen with `| null` when `.nullable()` is set.
|
|
|
2260
2389
|
|
|
2261
2390
|
## Pick a skill
|
|
2262
2391
|
|
|
2263
|
-
| If the task is about…
|
|
2264
|
-
|
|
|
2265
|
-
| Picking the right primitive (`v.string` vs `v.scalar`, `v.literal` vs `v.enum`, `v.date` vs `v.instanceof(Date)`)
|
|
2266
|
-
| Building object / array / record / tuple / union schemas, discriminated unions, recursive schemas
|
|
2267
|
-
| Modifiers — `.required` / `.optional` / `.nullable` / `.default` / `.catch` / `.omit`, transformer vs mutator pipelines, membership rules
|
|
2268
|
-
| Reading `ValidationResult`, branching on `error.type`, error message customization, translation
|
|
2269
|
-
| Generating JSON Schema for OpenAI strict / OpenAPI / draft-07
|
|
2270
|
-
| Why a `StandardSchemaV1<T>` slot accepts/rejects a schema,
|
|
2271
|
-
| Authoring custom seal plugins to add validator methods
|
|
2392
|
+
| If the task is about… | Load |
|
|
2393
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
2394
|
+
| Picking the right primitive (`v.string` vs `v.scalar`, `v.literal` vs `v.enum`, `v.date` vs `v.instanceof(Date)`) | The `pick-seal-primitive` topic |
|
|
2395
|
+
| Building object / array / record / tuple / union schemas, discriminated unions, recursive schemas | The `define-structural-shape` topic |
|
|
2396
|
+
| Modifiers — `.required` / `.optional` / `.nullable` / `.default` / `.catch` / `.omit`, transformer vs mutator pipelines, membership rules | The `compose-seal-modifiers` topic |
|
|
2397
|
+
| Reading `ValidationResult`, branching on `error.type`, error message customization, translation | The `handle-seal-errors` topic |
|
|
2398
|
+
| Generating JSON Schema for OpenAI strict / OpenAPI / draft-07 | The `generate-json-schema` topic |
|
|
2399
|
+
| Why a `StandardSchemaV1<T>` slot accepts/rejects a schema, object input/output class parameters and primitive factory intersections, `Result<unknown>` errors | The `bridge-standard-schema` topic |
|
|
2400
|
+
| Authoring custom seal plugins to add validator methods | The `extend-seal-with-plugins` topic |
|
|
2272
2401
|
|
|
2273
2402
|
## Things NOT to do
|
|
2274
2403
|
|