@warlock.js/seal 5.24.0 → 5.25.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 +11 -0
- package/LICENSE +21 -21
- package/cjs/index.cjs +10 -5
- package/cjs/index.cjs.map +1 -1
- package/cjs/{object-DPPttDAF.cjs → object-BuKApaax.cjs} +3 -3
- package/cjs/object-BuKApaax.cjs.map +1 -0
- package/cjs/object.cjs +2 -2
- package/cjs/{required-methods-DJTm7yNP.cjs → required-methods-BOVNRtci.cjs} +12 -2
- package/cjs/required-methods-BOVNRtci.cjs.map +1 -0
- package/cjs/{string-C_pAtZPC.cjs → string-DpDpgefH.cjs} +2 -2
- package/cjs/string-DpDpgefH.cjs.map +1 -0
- package/cjs/string.cjs +2 -2
- package/esm/config.d.mts +0 -1
- package/esm/config.d.mts.map +1 -1
- package/esm/config.mjs.map +1 -1
- package/esm/factory/validate.mjs.map +1 -1
- package/esm/factory/validators.d.mts.map +1 -1
- package/esm/factory/validators.mjs +1 -1
- package/esm/factory/validators.mjs.map +1 -1
- package/esm/helpers/date-helpers.mjs.map +1 -1
- package/esm/helpers/get-field-value.d.mts +0 -1
- package/esm/helpers/get-field-value.d.mts.map +1 -1
- package/esm/helpers/get-field-value.mjs.map +1 -1
- package/esm/helpers/index.d.mts +5 -0
- package/esm/helpers/path-helpers.mjs.map +1 -1
- package/esm/helpers/validation-helpers.d.mts +0 -1
- package/esm/helpers/validation-helpers.d.mts.map +1 -1
- package/esm/mutators/array-mutators.d.mts +0 -1
- package/esm/mutators/array-mutators.d.mts.map +1 -1
- package/esm/mutators/array-mutators.mjs.map +1 -1
- package/esm/mutators/boolean-mutators.d.mts +0 -1
- package/esm/mutators/boolean-mutators.d.mts.map +1 -1
- package/esm/mutators/date-mutators.d.mts +0 -1
- package/esm/mutators/date-mutators.d.mts.map +1 -1
- package/esm/mutators/date-mutators.mjs.map +1 -1
- package/esm/mutators/index.d.mts +7 -0
- package/esm/mutators/number-mutators.d.mts +0 -1
- package/esm/mutators/number-mutators.d.mts.map +1 -1
- package/esm/mutators/number-mutators.mjs.map +1 -1
- package/esm/mutators/object-mutators.d.mts +0 -1
- package/esm/mutators/object-mutators.d.mts.map +1 -1
- package/esm/mutators/object-mutators.mjs.map +1 -1
- package/esm/mutators/scalar-mutators.d.mts +0 -1
- package/esm/mutators/scalar-mutators.d.mts.map +1 -1
- package/esm/mutators/scalar-mutators.mjs.map +1 -1
- package/esm/mutators/string-mutators.d.mts +0 -1
- package/esm/mutators/string-mutators.d.mts.map +1 -1
- package/esm/mutators/string-mutators.mjs.map +1 -1
- package/esm/plugins/plugin-system.mjs.map +1 -1
- package/esm/rules/array/array-rules.d.mts +0 -1
- package/esm/rules/array/array-rules.d.mts.map +1 -1
- package/esm/rules/array/array-rules.mjs.map +1 -1
- package/esm/rules/array/index.d.mts +1 -0
- package/esm/rules/color/color-rules.d.mts +0 -1
- package/esm/rules/color/color-rules.d.mts.map +1 -1
- package/esm/rules/color/color-rules.mjs.map +1 -1
- package/esm/rules/color/index.d.mts +1 -0
- package/esm/rules/common/enum.d.mts +0 -1
- package/esm/rules/common/enum.d.mts.map +1 -1
- package/esm/rules/common/enum.mjs.map +1 -1
- package/esm/rules/common/equals-field-rules.d.mts +0 -1
- package/esm/rules/common/equals-field-rules.d.mts.map +1 -1
- package/esm/rules/common/equals-field-rules.mjs.map +1 -1
- package/esm/rules/common/index.d.mts +6 -0
- package/esm/rules/common/instanceof.d.mts +0 -1
- package/esm/rules/common/instanceof.d.mts.map +1 -1
- package/esm/rules/common/literal.d.mts +0 -1
- package/esm/rules/common/literal.d.mts.map +1 -1
- package/esm/rules/common/type-rules.d.mts +0 -1
- package/esm/rules/common/type-rules.d.mts.map +1 -1
- package/esm/rules/common/type-rules.mjs.map +1 -1
- package/esm/rules/common/unknown-key.d.mts +0 -1
- package/esm/rules/common/unknown-key.d.mts.map +1 -1
- package/esm/rules/conditional/forbidden-if-rules.d.mts +0 -1
- package/esm/rules/conditional/forbidden-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/forbidden-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/index.d.mts +10 -0
- package/esm/rules/conditional/present-if-rules.d.mts +0 -1
- package/esm/rules/conditional/present-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-unless-rules.d.mts +0 -1
- package/esm/rules/conditional/present-unless-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-unless-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-with-rules.d.mts +0 -1
- package/esm/rules/conditional/present-with-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-with-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-without-rules.d.mts +0 -1
- package/esm/rules/conditional/present-without-rules.d.mts.map +1 -1
- package/esm/rules/conditional/present-without-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-if-rules.d.mts +0 -1
- package/esm/rules/conditional/required-if-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-unless-rules.d.mts +0 -1
- package/esm/rules/conditional/required-unless-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-unless-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-when-rule.d.mts +0 -1
- package/esm/rules/conditional/required-when-rule.d.mts.map +1 -1
- package/esm/rules/conditional/required-with-rules.d.mts +0 -1
- package/esm/rules/conditional/required-with-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-with-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-without-rules.d.mts +0 -1
- package/esm/rules/conditional/required-without-rules.d.mts.map +1 -1
- package/esm/rules/conditional/required-without-rules.mjs.map +1 -1
- package/esm/rules/core/equal.d.mts +0 -1
- package/esm/rules/core/equal.d.mts.map +1 -1
- package/esm/rules/core/equal.mjs.map +1 -1
- package/esm/rules/core/forbidden.d.mts +0 -1
- package/esm/rules/core/forbidden.d.mts.map +1 -1
- package/esm/rules/core/forbidden.mjs.map +1 -1
- package/esm/rules/core/index.d.mts +5 -0
- package/esm/rules/core/required.d.mts +0 -1
- package/esm/rules/core/required.d.mts.map +1 -1
- package/esm/rules/core/required.mjs.map +1 -1
- package/esm/rules/core/union.d.mts.map +1 -1
- package/esm/rules/core/union.mjs.map +1 -1
- package/esm/rules/core/when.d.mts +0 -1
- 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 +0 -1
- package/esm/rules/date/date-comparison-rules.d.mts.map +1 -1
- package/esm/rules/date/date-comparison-rules.mjs.map +1 -1
- package/esm/rules/date/date-day-rules.d.mts +0 -1
- package/esm/rules/date/date-day-rules.d.mts.map +1 -1
- package/esm/rules/date/date-day-rules.mjs.map +1 -1
- package/esm/rules/date/date-field-comparison-rules.d.mts +0 -1
- package/esm/rules/date/date-field-comparison-rules.d.mts.map +1 -1
- package/esm/rules/date/date-field-comparison-rules.mjs.map +1 -1
- package/esm/rules/date/date-period-rules.d.mts +0 -1
- package/esm/rules/date/date-period-rules.d.mts.map +1 -1
- package/esm/rules/date/date-period-rules.mjs.map +1 -1
- package/esm/rules/date/date-relative-rules.d.mts +0 -1
- package/esm/rules/date/date-relative-rules.d.mts.map +1 -1
- package/esm/rules/date/date-relative-rules.mjs.map +1 -1
- package/esm/rules/date/date-special-rules.d.mts +0 -1
- package/esm/rules/date/date-special-rules.d.mts.map +1 -1
- package/esm/rules/date/date-special-rules.mjs.map +1 -1
- package/esm/rules/date/date.d.mts +0 -1
- package/esm/rules/date/date.d.mts.map +1 -1
- package/esm/rules/date/date.mjs.map +1 -1
- package/esm/rules/date/index.d.mts +7 -0
- package/esm/rules/file/dimensions.d.mts +0 -1
- package/esm/rules/file/dimensions.d.mts.map +1 -1
- package/esm/rules/file/dimensions.mjs.map +1 -1
- package/esm/rules/file/file-size.d.mts +0 -1
- package/esm/rules/file/file-size.d.mts.map +1 -1
- package/esm/rules/file/file-size.mjs.map +1 -1
- package/esm/rules/file/index.d.mts +2 -0
- package/esm/rules/index.d.mts +45 -0
- package/esm/rules/length/index.d.mts +1 -0
- package/esm/rules/length/length-rules.d.mts +0 -1
- package/esm/rules/length/length-rules.d.mts.map +1 -1
- package/esm/rules/length/length-rules.mjs.map +1 -1
- package/esm/rules/number/index.d.mts +1 -0
- package/esm/rules/number/number-rules.d.mts +0 -1
- package/esm/rules/number/number-rules.d.mts.map +1 -1
- package/esm/rules/number/number-rules.mjs.map +1 -1
- package/esm/rules/scalar/accepted-rule.mjs.map +1 -1
- package/esm/rules/scalar/declined-rule.mjs.map +1 -1
- package/esm/rules/string/alpha.d.mts +0 -1
- package/esm/rules/string/alpha.d.mts.map +1 -1
- package/esm/rules/string/alpha.mjs.map +1 -1
- package/esm/rules/string/credit-card.d.mts +0 -1
- package/esm/rules/string/credit-card.d.mts.map +1 -1
- package/esm/rules/string/credit-card.mjs.map +1 -1
- package/esm/rules/string/email.d.mts +0 -1
- package/esm/rules/string/email.d.mts.map +1 -1
- package/esm/rules/string/email.mjs.map +1 -1
- package/esm/rules/string/id-formats.d.mts +0 -1
- package/esm/rules/string/id-formats.d.mts.map +1 -1
- package/esm/rules/string/index.d.mts +11 -0
- package/esm/rules/string/ip.d.mts +0 -1
- package/esm/rules/string/ip.d.mts.map +1 -1
- package/esm/rules/string/matches.d.mts +0 -1
- package/esm/rules/string/matches.d.mts.map +1 -1
- package/esm/rules/string/matches.mjs.map +1 -1
- package/esm/rules/string/pattern.d.mts +0 -1
- package/esm/rules/string/pattern.d.mts.map +1 -1
- package/esm/rules/string/pattern.mjs.map +1 -1
- package/esm/rules/string/string-comparison.d.mts +0 -1
- package/esm/rules/string/string-comparison.d.mts.map +1 -1
- package/esm/rules/string/string-comparison.mjs.map +1 -1
- package/esm/rules/string/strong-password-rule.d.mts +0 -1
- package/esm/rules/string/strong-password-rule.d.mts.map +1 -1
- package/esm/rules/string/strong-password-rule.mjs.map +1 -1
- package/esm/rules/string/url.d.mts +0 -1
- package/esm/rules/string/url.d.mts.map +1 -1
- package/esm/rules/string/url.mjs.map +1 -1
- package/esm/rules/string/without-whitespace.d.mts +0 -1
- package/esm/rules/string/without-whitespace.d.mts.map +1 -1
- package/esm/rules/string/without-whitespace.mjs.map +1 -1
- package/esm/standard-schema/index.d.mts +3 -0
- package/esm/standard-schema/map-result.d.mts +0 -1
- package/esm/standard-schema/map-result.d.mts.map +1 -1
- package/esm/types/date-types.mjs.map +1 -1
- package/esm/types/index.d.mts +10 -0
- package/esm/validators/any-validator.mjs.map +1 -1
- package/esm/validators/array-validator.d.mts.map +1 -1
- package/esm/validators/array-validator.mjs +1 -0
- package/esm/validators/array-validator.mjs.map +1 -1
- package/esm/validators/base-validator.d.mts +2 -1
- package/esm/validators/base-validator.d.mts.map +1 -1
- package/esm/validators/base-validator.mjs +11 -1
- package/esm/validators/base-validator.mjs.map +1 -1
- package/esm/validators/boolean-validator.d.mts.map +1 -1
- package/esm/validators/boolean-validator.mjs +1 -0
- package/esm/validators/boolean-validator.mjs.map +1 -1
- package/esm/validators/computed-validator.d.mts.map +1 -1
- package/esm/validators/date-validator.d.mts +0 -1
- package/esm/validators/date-validator.d.mts.map +1 -1
- package/esm/validators/date-validator.mjs +1 -0
- package/esm/validators/date-validator.mjs.map +1 -1
- package/esm/validators/discriminated-union-validator.d.mts.map +1 -1
- package/esm/validators/float-validator.mjs.map +1 -1
- package/esm/validators/int-validator.mjs.map +1 -1
- package/esm/validators/lazy-validator.d.mts.map +1 -1
- package/esm/validators/literal-validator.d.mts.map +1 -1
- package/esm/validators/literal-validator.mjs +1 -3
- package/esm/validators/literal-validator.mjs.map +1 -1
- package/esm/validators/managed-validator.d.mts.map +1 -1
- package/esm/validators/methods/equality-conditional-methods.d.mts +0 -1
- package/esm/validators/methods/equality-conditional-methods.d.mts.map +1 -1
- package/esm/validators/methods/equality-conditional-methods.mjs.map +1 -1
- package/esm/validators/methods/forbidden-methods.mjs.map +1 -1
- package/esm/validators/methods/present-methods.mjs.map +1 -1
- package/esm/validators/methods/required-methods.d.mts +0 -1
- package/esm/validators/methods/required-methods.d.mts.map +1 -1
- package/esm/validators/methods/required-methods.mjs.map +1 -1
- package/esm/validators/number-validator.d.mts.map +1 -1
- package/esm/validators/number-validator.mjs +1 -0
- package/esm/validators/number-validator.mjs.map +1 -1
- package/esm/validators/numeric-validator.mjs.map +1 -1
- package/esm/validators/object-validator.d.mts.map +1 -1
- package/esm/validators/object-validator.mjs +1 -1
- package/esm/validators/object-validator.mjs.map +1 -1
- package/esm/validators/record-validator.d.mts.map +1 -1
- package/esm/validators/record-validator.mjs.map +1 -1
- package/esm/validators/scalar-validator.d.mts.map +1 -1
- package/esm/validators/scalar-validator.mjs +1 -0
- package/esm/validators/scalar-validator.mjs.map +1 -1
- package/esm/validators/string-validator.mjs.map +1 -1
- package/esm/validators/tuple-validator.d.mts.map +1 -1
- package/esm/validators/tuple-validator.mjs.map +1 -1
- package/esm/validators/union-validator.mjs.map +1 -1
- package/llms-full.txt +1165 -1165
- package/package.json +1 -1
- package/skills/bridge-standard-schema/SKILL.md +138 -138
- package/skills/define-structural-shape/SKILL.md +138 -138
- package/skills/extend-seal-with-plugins/SKILL.md +178 -178
- package/skills/generate-json-schema/SKILL.md +143 -143
- package/skills/handle-seal-errors/SKILL.md +148 -148
- package/skills/pick-seal-primitive/SKILL.md +149 -149
- package/cjs/object-DPPttDAF.cjs.map +0 -1
- package/cjs/required-methods-DJTm7yNP.cjs.map +0 -1
- package/cjs/string-C_pAtZPC.cjs.map +0 -1
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: handle-seal-errors
|
|
3
|
-
description: 'Read `ValidationResult` — `isValid`, `errors[]`, `data`. Branch on `error.type`, customize messages, hook translation. Triggers: `ValidationResult`, `ValidationError`, `validate`, `isValid`, `errors`, `data`, `error.type`, `error.input`, `error.error`, `translationParams`, `.attribute`, `SealConfig`; "how to read seal errors", "branch on a specific rule failure", "customize validation error message", "translate seal error", "surface validation errors as 422"; typical import `import { validate, v } from "@warlock.js/seal"`. Skip: modifiers — `@warlock.js/seal/compose-seal-modifiers/SKILL.md`; structural shapes — `@warlock.js/seal/define-structural-shape/SKILL.md`; competing libs `zod` `.safeParse`, `yup` `ValidationError`, `joi`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# `ValidationResult` — reading errors
|
|
7
|
-
|
|
8
|
-
`validate(schema, data)` never throws. It returns a `ValidationResult`:
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
type ValidationResult = {
|
|
12
|
-
isValid: boolean;
|
|
13
|
-
data: any; // shape after mutators + transformers (the validated value)
|
|
14
|
-
errors: ValidationError[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type ValidationError = {
|
|
18
|
-
type: string; // rule type that failed — "required", "email", "min", "string", ...
|
|
19
|
-
error: string; // resolved message (translated, with attribute substitution)
|
|
20
|
-
input: string; // input field name — "email" or "address.city"
|
|
21
|
-
};
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Branch on `isValid` first; reach for `errors[]` only when you need to act on a specific failure.
|
|
25
|
-
|
|
26
|
-
## Basic flow
|
|
27
|
-
|
|
28
|
-
```ts
|
|
29
|
-
import { validate, v } from "@warlock.js/seal";
|
|
30
|
-
|
|
31
|
-
const schema = v.object({
|
|
32
|
-
email: v.string().email(),
|
|
33
|
-
age: v.int().min(13).optional(),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const result = await validate(schema, input);
|
|
37
|
-
|
|
38
|
-
if (result.isValid) {
|
|
39
|
-
return result.data; // typed as the inferred shape (post-transformer)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
status: 422,
|
|
44
|
-
body: { errors: result.errors },
|
|
45
|
-
};
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Branching on a specific rule
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
const result = await validate(schema, input);
|
|
52
|
-
|
|
53
|
-
if (!result.isValid) {
|
|
54
|
-
const emailMissing = result.errors.find(
|
|
55
|
-
e => e.input === "email" && e.type === "required",
|
|
56
|
-
);
|
|
57
|
-
if (emailMissing) {
|
|
58
|
-
return { redirect: "/signup", reason: "no email" };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const ageInvalid = result.errors.find(
|
|
62
|
-
e => e.input === "age" && (e.type === "int" || e.type === "min"),
|
|
63
|
-
);
|
|
64
|
-
if (ageInvalid) {
|
|
65
|
-
return { error: "Age must be 13 or older" };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
The `type` field is the **stable** identifier — the message is human-facing and may be localized. Branch on `type`, never on the message string.
|
|
71
|
-
|
|
72
|
-
## Common rule type names
|
|
73
|
-
|
|
74
|
-
These are the strings you'll see in `error.type`:
|
|
75
|
-
|
|
76
|
-
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:
|
|
77
|
-
|
|
78
|
-
| Type | Produced by |
|
|
79
|
-
| --- | --- |
|
|
80
|
-
| `required`, `present` | `.required()`, `.present()` (note: `.optional()` has no error type — it clears the required rule) |
|
|
81
|
-
| `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless` | conditional required methods |
|
|
82
|
-
| `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date` | type guards from `matchesType` |
|
|
83
|
-
| `minLength`, `maxLength`, `betweenLength`, `length` | string / array length rules (`.min`/`.max` on a string alias to `minLength`/`maxLength`) |
|
|
84
|
-
| `min`, `max`, `betweenNumbers` | number range rules (`.min`/`.max`/`.between` on a number) |
|
|
85
|
-
| `email`, `url`, `uuid`, `pattern`, `matches` | string format rules (`.pattern()` → `pattern`) |
|
|
86
|
-
| `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues` | value-membership rules (`.oneOf` aliases `in`; `.notIn`/`.forbids` → `notAllowedValues`; `.allowsOnly` → `allowedValues`) |
|
|
87
|
-
| `instanceof` | `v.instanceof(Ctor)` |
|
|
88
|
-
| `equalsField`, `notEqualsField` | cross-field equality (`.sameAs` → `equalsField`; `.differentFrom` → `notEqualsField`) |
|
|
89
|
-
| `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`) |
|
|
90
|
-
|
|
91
|
-
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.
|
|
92
|
-
|
|
93
|
-
## Customizing error messages
|
|
94
|
-
|
|
95
|
-
Two layers:
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
v.string().email("Please enter a valid email address").required("Email is required");
|
|
99
|
-
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
|
|
100
|
-
// per-rule override per-rule override
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Each chain method takes an optional `errorMessage` as its last argument. That overrides the rule's `defaultErrorMessage`. Use it when a single rule needs a tailored message in a specific schema.
|
|
104
|
-
|
|
105
|
-
For project-wide message overrides, hook into the translation layer (the framework calls `resolveTranslation` with the rule context — wire up your own `t()` function via `SealConfig`).
|
|
106
|
-
|
|
107
|
-
## Translation params
|
|
108
|
-
|
|
109
|
-
Rules can stash dynamic substitution params on the context. The default messages reference them:
|
|
110
|
-
|
|
111
|
-
```
|
|
112
|
-
"The :input must be at least :min characters"
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
`:input` is the field name (or its translated display name); `:min` and others come from rule-specific metadata. If you need to render the message yourself in a custom UI, the params are available on the rule's context.
|
|
116
|
-
|
|
117
|
-
For attribute display names ("email" → "Email Address"), use `.attributes({ email: "Email Address" })` on the parent `v.object` or pass via the `validate()` options. The `:input` placeholder picks up the configured display name.
|
|
118
|
-
|
|
119
|
-
## When to throw
|
|
120
|
-
|
|
121
|
-
Don't wrap `validate()` in try/catch for *validation* failures — those land in `result.errors`. The only thing that *throws* is a programming bug:
|
|
122
|
-
|
|
123
|
-
- A rule's callback threw (e.g. you wrote `async validate() { throw new Error(...) }`).
|
|
124
|
-
- A transformer threw on output.
|
|
125
|
-
- A mutator threw on input.
|
|
126
|
-
|
|
127
|
-
Those are bugs — fix them. Don't try/catch them in app code as a way to handle bad input.
|
|
128
|
-
|
|
129
|
-
## At the framework boundary
|
|
130
|
-
|
|
131
|
-
If you're surfacing seal errors through HTTP / RPC, the typical shape is:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
if (!result.isValid) {
|
|
135
|
-
return reply.code(422).send({
|
|
136
|
-
error: "validation_failed",
|
|
137
|
-
fields: result.errors.map(e => ({
|
|
138
|
-
field: e.input,
|
|
139
|
-
type: e.type,
|
|
140
|
-
message: e.error,
|
|
141
|
-
})),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Avoid leaking `result.data` back to the client when rejecting — it might contain transformed sensitive fields.
|
|
147
|
-
|
|
148
|
-
For server-side logs, log `errors[]` with the field paths and rule types; redact values unless you're certain the field isn't sensitive. The `@warlock.js/logger` redaction layer is the right place to enforce that.
|
|
1
|
+
---
|
|
2
|
+
name: handle-seal-errors
|
|
3
|
+
description: 'Read `ValidationResult` — `isValid`, `errors[]`, `data`. Branch on `error.type`, customize messages, hook translation. Triggers: `ValidationResult`, `ValidationError`, `validate`, `isValid`, `errors`, `data`, `error.type`, `error.input`, `error.error`, `translationParams`, `.attribute`, `SealConfig`; "how to read seal errors", "branch on a specific rule failure", "customize validation error message", "translate seal error", "surface validation errors as 422"; typical import `import { validate, v } from "@warlock.js/seal"`. Skip: modifiers — `@warlock.js/seal/compose-seal-modifiers/SKILL.md`; structural shapes — `@warlock.js/seal/define-structural-shape/SKILL.md`; competing libs `zod` `.safeParse`, `yup` `ValidationError`, `joi`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `ValidationResult` — reading errors
|
|
7
|
+
|
|
8
|
+
`validate(schema, data)` never throws. It returns a `ValidationResult`:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
type ValidationResult = {
|
|
12
|
+
isValid: boolean;
|
|
13
|
+
data: any; // shape after mutators + transformers (the validated value)
|
|
14
|
+
errors: ValidationError[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ValidationError = {
|
|
18
|
+
type: string; // rule type that failed — "required", "email", "min", "string", ...
|
|
19
|
+
error: string; // resolved message (translated, with attribute substitution)
|
|
20
|
+
input: string; // input field name — "email" or "address.city"
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Branch on `isValid` first; reach for `errors[]` only when you need to act on a specific failure.
|
|
25
|
+
|
|
26
|
+
## Basic flow
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { validate, v } from "@warlock.js/seal";
|
|
30
|
+
|
|
31
|
+
const schema = v.object({
|
|
32
|
+
email: v.string().email(),
|
|
33
|
+
age: v.int().min(13).optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = await validate(schema, input);
|
|
37
|
+
|
|
38
|
+
if (result.isValid) {
|
|
39
|
+
return result.data; // typed as the inferred shape (post-transformer)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
status: 422,
|
|
44
|
+
body: { errors: result.errors },
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Branching on a specific rule
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const result = await validate(schema, input);
|
|
52
|
+
|
|
53
|
+
if (!result.isValid) {
|
|
54
|
+
const emailMissing = result.errors.find(
|
|
55
|
+
e => e.input === "email" && e.type === "required",
|
|
56
|
+
);
|
|
57
|
+
if (emailMissing) {
|
|
58
|
+
return { redirect: "/signup", reason: "no email" };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const ageInvalid = result.errors.find(
|
|
62
|
+
e => e.input === "age" && (e.type === "int" || e.type === "min"),
|
|
63
|
+
);
|
|
64
|
+
if (ageInvalid) {
|
|
65
|
+
return { error: "Age must be 13 or older" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `type` field is the **stable** identifier — the message is human-facing and may be localized. Branch on `type`, never on the message string.
|
|
71
|
+
|
|
72
|
+
## Common rule type names
|
|
73
|
+
|
|
74
|
+
These are the strings you'll see in `error.type`:
|
|
75
|
+
|
|
76
|
+
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:
|
|
77
|
+
|
|
78
|
+
| Type | Produced by |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `required`, `present` | `.required()`, `.present()` (note: `.optional()` has no error type — it clears the required rule) |
|
|
81
|
+
| `requiredIf`, `requiredWith`, `requiredWithout`, `requiredUnless` | conditional required methods |
|
|
82
|
+
| `string`, `number`, `int`, `float`, `boolean`, `scalar`, `object`, `array`, `date` | type guards from `matchesType` |
|
|
83
|
+
| `minLength`, `maxLength`, `betweenLength`, `length` | string / array length rules (`.min`/`.max` on a string alias to `minLength`/`maxLength`) |
|
|
84
|
+
| `min`, `max`, `betweenNumbers` | number range rules (`.min`/`.max`/`.between` on a number) |
|
|
85
|
+
| `email`, `url`, `uuid`, `pattern`, `matches` | string format rules (`.pattern()` → `pattern`) |
|
|
86
|
+
| `literal`, `enum`, `in`, `allowedValues`, `notAllowedValues` | value-membership rules (`.oneOf` aliases `in`; `.notIn`/`.forbids` → `notAllowedValues`; `.allowsOnly` → `allowedValues`) |
|
|
87
|
+
| `instanceof` | `v.instanceof(Ctor)` |
|
|
88
|
+
| `equalsField`, `notEqualsField` | cross-field equality (`.sameAs` → `equalsField`; `.differentFrom` → `notEqualsField`) |
|
|
89
|
+
| `minDate`, `maxDate`, `beforeField`, `afterField`, `today`, `past`, `future`, `weekDay`, `weekend`, `businessDay`, `birthday` | date rules (`.min`/`.max` → `minDate`/`maxDate`; `.before`/`.after` → `beforeField`/`afterField`) |
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
## Customizing error messages
|
|
94
|
+
|
|
95
|
+
Two layers:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
v.string().email("Please enter a valid email address").required("Email is required");
|
|
99
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
|
|
100
|
+
// per-rule override per-rule override
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each chain method takes an optional `errorMessage` as its last argument. That overrides the rule's `defaultErrorMessage`. Use it when a single rule needs a tailored message in a specific schema.
|
|
104
|
+
|
|
105
|
+
For project-wide message overrides, hook into the translation layer (the framework calls `resolveTranslation` with the rule context — wire up your own `t()` function via `SealConfig`).
|
|
106
|
+
|
|
107
|
+
## Translation params
|
|
108
|
+
|
|
109
|
+
Rules can stash dynamic substitution params on the context. The default messages reference them:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
"The :input must be at least :min characters"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`:input` is the field name (or its translated display name); `:min` and others come from rule-specific metadata. If you need to render the message yourself in a custom UI, the params are available on the rule's context.
|
|
116
|
+
|
|
117
|
+
For attribute display names ("email" → "Email Address"), use `.attributes({ email: "Email Address" })` on the parent `v.object` or pass via the `validate()` options. The `:input` placeholder picks up the configured display name.
|
|
118
|
+
|
|
119
|
+
## When to throw
|
|
120
|
+
|
|
121
|
+
Don't wrap `validate()` in try/catch for *validation* failures — those land in `result.errors`. The only thing that *throws* is a programming bug:
|
|
122
|
+
|
|
123
|
+
- A rule's callback threw (e.g. you wrote `async validate() { throw new Error(...) }`).
|
|
124
|
+
- A transformer threw on output.
|
|
125
|
+
- A mutator threw on input.
|
|
126
|
+
|
|
127
|
+
Those are bugs — fix them. Don't try/catch them in app code as a way to handle bad input.
|
|
128
|
+
|
|
129
|
+
## At the framework boundary
|
|
130
|
+
|
|
131
|
+
If you're surfacing seal errors through HTTP / RPC, the typical shape is:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
if (!result.isValid) {
|
|
135
|
+
return reply.code(422).send({
|
|
136
|
+
error: "validation_failed",
|
|
137
|
+
fields: result.errors.map(e => ({
|
|
138
|
+
field: e.input,
|
|
139
|
+
type: e.type,
|
|
140
|
+
message: e.error,
|
|
141
|
+
})),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Avoid leaking `result.data` back to the client when rejecting — it might contain transformed sensitive fields.
|
|
147
|
+
|
|
148
|
+
For server-side logs, log `errors[]` with the field paths and rule types; redact values unless you're certain the field isn't sensitive. The `@warlock.js/logger` redaction layer is the right place to enforce that.
|
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pick-seal-primitive
|
|
3
|
-
description: 'Pick the right `v` factory primitive — string / int / literal / date / enum / computed / managed / instanceof / any. Triggers: `v.string`, `v.email`, `v.number`, `v.int`, `v.float`, `v.numeric`, `v.boolean`, `v.scalar`, `v.date`, `v.literal`, `v.enum`, `v.instanceof`, `v.computed`, `v.managed`, `v.any`; "v.string vs v.scalar", "v.literal vs v.enum", "v.date vs v.instanceof(Date)", "what is v.computed"; typical import `import { v } from "@warlock.js/seal"`. Skip: structural shapes — `@warlock.js/seal/define-structural-shape/SKILL.md`; modifiers — `@warlock.js/seal/compose-seal-modifiers/SKILL.md`; competing libs `zod`, `valibot`, `yup`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Picking the right primitive
|
|
7
|
-
|
|
8
|
-
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.
|
|
9
|
-
|
|
10
|
-
## Strings
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
v.string() // type: string — full surface in string-methods.md
|
|
14
|
-
v.email() // shorthand for v.string().email()
|
|
15
|
-
v.enum(["a", "b"]) // type: "a" | "b" — runs as v.string().oneOf, but the factory overload preserves the literal union
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
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.
|
|
19
|
-
|
|
20
|
-
## Numbers — pick by what you accept
|
|
21
|
-
|
|
22
|
-
| Validator | Accepts | When |
|
|
23
|
-
|---|---|---|
|
|
24
|
-
| `v.number()` | any finite number | accepts both integers and floats |
|
|
25
|
-
| `v.int()` | integers only | rejects `1.5` |
|
|
26
|
-
| `v.float()` | finite, non-integer | rejects `1` |
|
|
27
|
-
| `v.numeric()` | numeric strings + numbers | form/query inputs that arrive as `"42"` — coerces to number |
|
|
28
|
-
|
|
29
|
-
All four share the same chain surface (see [`number-methods.md`](./number-methods.md)). Picking is about input acceptance, not chain power.
|
|
30
|
-
|
|
31
|
-
## Booleans & scalars
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
v.boolean() // type: boolean — adds .accepted() / .declined() for form-style inputs
|
|
35
|
-
v.scalar() // type: string | number | boolean — usually a smell pointing at a missing discriminator
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
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.
|
|
39
|
-
|
|
40
|
-
## Dates
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
v.date() // type: Date — normalizes strings/timestamps to Date, rich rule surface
|
|
44
|
-
v.instanceof(Date) // type: Date — raw instanceof, no normalization, no rules
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
`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.
|
|
48
|
-
|
|
49
|
-
## Literals & instances
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
v.literal("items") // type: "items"
|
|
53
|
-
v.literal("draft", "published", "archived") // type: "draft" | "published" | "archived"
|
|
54
|
-
v.literal(1, 2, 3) // type: 1 | 2 | 3
|
|
55
|
-
v.literal(true) // type: true
|
|
56
|
-
|
|
57
|
-
v.instanceof(File) // type: File
|
|
58
|
-
v.instanceof(Buffer) // type: Buffer
|
|
59
|
-
v.instanceof(MyClass) // type: MyClass
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**`v.literal` vs `v.string().oneOf([...])` vs `v.enum([...])`:**
|
|
63
|
-
|
|
64
|
-
- `v.literal("a", "b")` infers as `"a" | "b"` (literal narrowing). **Use this for discriminator fields.**
|
|
65
|
-
- `v.string().oneOf(["a", "b"])` infers as `string` (loses literal types). Use when broad type is fine.
|
|
66
|
-
- `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]`.
|
|
67
|
-
|
|
68
|
-
`**`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*:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
const decorative = v.object({ decorative: v.literal(true), alt: v.literal("") });
|
|
72
|
-
|
|
73
|
-
await v.validate(decorative, { decorative: true, alt: "" }); // valid
|
|
74
|
-
await v.validate(decorative, { decorative: true, alt: "text" }); // invalid — not the literal
|
|
75
|
-
await v.validate(decorative, { decorative: true }); // invalid — key missing
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
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.
|
|
79
|
-
|
|
80
|
-
:::note[Fixed in 4.9.2]
|
|
81
|
-
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.
|
|
82
|
-
:::
|
|
83
|
-
|
|
84
|
-
`v.instanceof(Ctor)` for File/Buffer/Uint8Array/custom classes. Returns `{}` from `toJsonSchema()` (not representable). For OpenAPI `File`, attach `{ type: "string", format: "binary" }` manually after generation.
|
|
85
|
-
|
|
86
|
-
## `v.any` — escape hatch
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
v.any() // type: any — skips validation entirely
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
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.
|
|
93
|
-
|
|
94
|
-
## Derived: `v.computed` and `v.managed`
|
|
95
|
-
|
|
96
|
-
These two **don't validate input** — they produce a value as part of validation.
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
v.object({
|
|
100
|
-
firstName: v.string(),
|
|
101
|
-
lastName: v.string(),
|
|
102
|
-
fullName: v.computed<string>((data) => `${data.firstName} ${data.lastName}`),
|
|
103
|
-
createdAt: v.managed<Date>(() => new Date()),
|
|
104
|
-
createdBy: v.managed<string>((context) => context.context?.userId),
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
- **`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))`.
|
|
109
|
-
- **`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.
|
|
110
|
-
|
|
111
|
-
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).
|
|
112
|
-
|
|
113
|
-
## Quick map — "I need to validate…"
|
|
114
|
-
|
|
115
|
-
| Need | Reach for |
|
|
116
|
-
|---|---|
|
|
117
|
-
| Email | `v.string().email()` or `v.email()` |
|
|
118
|
-
| URL | `v.string().url()` |
|
|
119
|
-
| UUID | `v.string().uuid()` (see [`string-methods.md`](./string-methods.md)) |
|
|
120
|
-
| Number 0–100 | `v.number().between(0, 100)` |
|
|
121
|
-
| Positive integer | `v.int().positive()` |
|
|
122
|
-
| One of N constants | `v.literal(...values)` |
|
|
123
|
-
| One of TS enum values | `v.enum(MyEnum)` |
|
|
124
|
-
| Date in the past | `v.date().past()` |
|
|
125
|
-
| File upload | `v.instanceof(File)` |
|
|
126
|
-
| Class instance (not Date) | `v.instanceof(Ctor)` |
|
|
127
|
-
| Discriminated union | see [`@warlock.js/seal/define-structural-shape/SKILL.md`](@warlock.js/seal/define-structural-shape/SKILL.md) |
|
|
128
|
-
| Derived value (computed from siblings) | `v.computed<T>(callback)` |
|
|
129
|
-
| Framework-injected value | `v.managed<T>(callback)` |
|
|
130
|
-
| Free-form / pass-through | `v.any()` (only when you've thought about it) |
|
|
131
|
-
|
|
132
|
-
## Method-surface reference
|
|
133
|
-
|
|
134
|
-
Each primitive's full method list lives in a sibling file:
|
|
135
|
-
|
|
136
|
-
- [`string-methods.md`](./string-methods.md) — `.email` / `.url` / `.uuid` / `.pattern` / `.startsWith` / `.alpha` / `.trim` / `.slug` / `.mask` / `.base64Encode` / …
|
|
137
|
-
- [`number-methods.md`](./number-methods.md) — `.min` / `.max` / `.between` / `.greaterThan` / `.positive` / `.even` / `.multipleOf` / `.minSibling` / `.round` / …
|
|
138
|
-
- [`date-methods.md`](./date-methods.md) — `.min` / `.before` / `.after` / `.today` / `.past` / `.future` / `.weekDay` / `.minAge` / `.year` / `.quarter` / `.toISOString` / …
|
|
139
|
-
- [`boolean-methods.md`](./boolean-methods.md) — `.accepted` / `.declined` / `.mustBeTrue` / `.mustBeFalse` / `.acceptedIf` / `.declinedWithout` / …
|
|
140
|
-
|
|
141
|
-
For cross-cutting modifiers (`.optional`/`.nullable`/`.default`/`.catch`/`.in`/`.oneOf`), see [`@warlock.js/seal/compose-seal-modifiers/SKILL.md`](@warlock.js/seal/compose-seal-modifiers/SKILL.md). For containers (object/array/record/tuple/union), see [`@warlock.js/seal/define-structural-shape/SKILL.md`](@warlock.js/seal/define-structural-shape/SKILL.md).
|
|
142
|
-
|
|
143
|
-
## Things NOT to do
|
|
144
|
-
|
|
145
|
-
- Don't `new ObjectValidator()` (or any class) directly — factory returns carry the StandardSchema bridge that bare instantiation loses.
|
|
146
|
-
- 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.
|
|
147
|
-
- Don't reach for `v.instanceof(Date)` when `v.date()` works. The latter is purpose-built.
|
|
148
|
-
- Don't use `v.string().oneOf(["a", "b"])` for discriminator fields where you need the literal type — use `v.literal("a", "b")`.
|
|
149
|
-
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when *producing* a value.
|
|
1
|
+
---
|
|
2
|
+
name: pick-seal-primitive
|
|
3
|
+
description: 'Pick the right `v` factory primitive — string / int / literal / date / enum / computed / managed / instanceof / any. Triggers: `v.string`, `v.email`, `v.number`, `v.int`, `v.float`, `v.numeric`, `v.boolean`, `v.scalar`, `v.date`, `v.literal`, `v.enum`, `v.instanceof`, `v.computed`, `v.managed`, `v.any`; "v.string vs v.scalar", "v.literal vs v.enum", "v.date vs v.instanceof(Date)", "what is v.computed"; typical import `import { v } from "@warlock.js/seal"`. Skip: structural shapes — `@warlock.js/seal/define-structural-shape/SKILL.md`; modifiers — `@warlock.js/seal/compose-seal-modifiers/SKILL.md`; competing libs `zod`, `valibot`, `yup`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Picking the right primitive
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
## Strings
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
v.string() // type: string — full surface in string-methods.md
|
|
14
|
+
v.email() // shorthand for v.string().email()
|
|
15
|
+
v.enum(["a", "b"]) // type: "a" | "b" — runs as v.string().oneOf, but the factory overload preserves the literal union
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
## Numbers — pick by what you accept
|
|
21
|
+
|
|
22
|
+
| Validator | Accepts | When |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `v.number()` | any finite number | accepts both integers and floats |
|
|
25
|
+
| `v.int()` | integers only | rejects `1.5` |
|
|
26
|
+
| `v.float()` | finite, non-integer | rejects `1` |
|
|
27
|
+
| `v.numeric()` | numeric strings + numbers | form/query inputs that arrive as `"42"` — coerces to number |
|
|
28
|
+
|
|
29
|
+
All four share the same chain surface (see [`number-methods.md`](./number-methods.md)). Picking is about input acceptance, not chain power.
|
|
30
|
+
|
|
31
|
+
## Booleans & scalars
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
v.boolean() // type: boolean — adds .accepted() / .declined() for form-style inputs
|
|
35
|
+
v.scalar() // type: string | number | boolean — usually a smell pointing at a missing discriminator
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
## Dates
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
v.date() // type: Date — normalizes strings/timestamps to Date, rich rule surface
|
|
44
|
+
v.instanceof(Date) // type: Date — raw instanceof, no normalization, no rules
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`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.
|
|
48
|
+
|
|
49
|
+
## Literals & instances
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
v.literal("items") // type: "items"
|
|
53
|
+
v.literal("draft", "published", "archived") // type: "draft" | "published" | "archived"
|
|
54
|
+
v.literal(1, 2, 3) // type: 1 | 2 | 3
|
|
55
|
+
v.literal(true) // type: true
|
|
56
|
+
|
|
57
|
+
v.instanceof(File) // type: File
|
|
58
|
+
v.instanceof(Buffer) // type: Buffer
|
|
59
|
+
v.instanceof(MyClass) // type: MyClass
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**`v.literal` vs `v.string().oneOf([...])` vs `v.enum([...])`:**
|
|
63
|
+
|
|
64
|
+
- `v.literal("a", "b")` infers as `"a" | "b"` (literal narrowing). **Use this for discriminator fields.**
|
|
65
|
+
- `v.string().oneOf(["a", "b"])` infers as `string` (loses literal types). Use when broad type is fine.
|
|
66
|
+
- `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]`.
|
|
67
|
+
|
|
68
|
+
`**`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*:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const decorative = v.object({ decorative: v.literal(true), alt: v.literal("") });
|
|
72
|
+
|
|
73
|
+
await v.validate(decorative, { decorative: true, alt: "" }); // valid
|
|
74
|
+
await v.validate(decorative, { decorative: true, alt: "text" }); // invalid — not the literal
|
|
75
|
+
await v.validate(decorative, { decorative: true }); // invalid — key missing
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
:::note[Fixed in 4.9.2]
|
|
81
|
+
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.
|
|
82
|
+
:::
|
|
83
|
+
|
|
84
|
+
`v.instanceof(Ctor)` for File/Buffer/Uint8Array/custom classes. Returns `{}` from `toJsonSchema()` (not representable). For OpenAPI `File`, attach `{ type: "string", format: "binary" }` manually after generation.
|
|
85
|
+
|
|
86
|
+
## `v.any` — escape hatch
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
v.any() // type: any — skips validation entirely
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
## Derived: `v.computed` and `v.managed`
|
|
95
|
+
|
|
96
|
+
These two **don't validate input** — they produce a value as part of validation.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
v.object({
|
|
100
|
+
firstName: v.string(),
|
|
101
|
+
lastName: v.string(),
|
|
102
|
+
fullName: v.computed<string>((data) => `${data.firstName} ${data.lastName}`),
|
|
103
|
+
createdAt: v.managed<Date>(() => new Date()),
|
|
104
|
+
createdBy: v.managed<string>((context) => context.context?.userId),
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- **`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))`.
|
|
109
|
+
- **`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.
|
|
110
|
+
|
|
111
|
+
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).
|
|
112
|
+
|
|
113
|
+
## Quick map — "I need to validate…"
|
|
114
|
+
|
|
115
|
+
| Need | Reach for |
|
|
116
|
+
|---|---|
|
|
117
|
+
| Email | `v.string().email()` or `v.email()` |
|
|
118
|
+
| URL | `v.string().url()` |
|
|
119
|
+
| UUID | `v.string().uuid()` (see [`string-methods.md`](./string-methods.md)) |
|
|
120
|
+
| Number 0–100 | `v.number().between(0, 100)` |
|
|
121
|
+
| Positive integer | `v.int().positive()` |
|
|
122
|
+
| One of N constants | `v.literal(...values)` |
|
|
123
|
+
| One of TS enum values | `v.enum(MyEnum)` |
|
|
124
|
+
| Date in the past | `v.date().past()` |
|
|
125
|
+
| File upload | `v.instanceof(File)` |
|
|
126
|
+
| Class instance (not Date) | `v.instanceof(Ctor)` |
|
|
127
|
+
| Discriminated union | see [`@warlock.js/seal/define-structural-shape/SKILL.md`](@warlock.js/seal/define-structural-shape/SKILL.md) |
|
|
128
|
+
| Derived value (computed from siblings) | `v.computed<T>(callback)` |
|
|
129
|
+
| Framework-injected value | `v.managed<T>(callback)` |
|
|
130
|
+
| Free-form / pass-through | `v.any()` (only when you've thought about it) |
|
|
131
|
+
|
|
132
|
+
## Method-surface reference
|
|
133
|
+
|
|
134
|
+
Each primitive's full method list lives in a sibling file:
|
|
135
|
+
|
|
136
|
+
- [`string-methods.md`](./string-methods.md) — `.email` / `.url` / `.uuid` / `.pattern` / `.startsWith` / `.alpha` / `.trim` / `.slug` / `.mask` / `.base64Encode` / …
|
|
137
|
+
- [`number-methods.md`](./number-methods.md) — `.min` / `.max` / `.between` / `.greaterThan` / `.positive` / `.even` / `.multipleOf` / `.minSibling` / `.round` / …
|
|
138
|
+
- [`date-methods.md`](./date-methods.md) — `.min` / `.before` / `.after` / `.today` / `.past` / `.future` / `.weekDay` / `.minAge` / `.year` / `.quarter` / `.toISOString` / …
|
|
139
|
+
- [`boolean-methods.md`](./boolean-methods.md) — `.accepted` / `.declined` / `.mustBeTrue` / `.mustBeFalse` / `.acceptedIf` / `.declinedWithout` / …
|
|
140
|
+
|
|
141
|
+
For cross-cutting modifiers (`.optional`/`.nullable`/`.default`/`.catch`/`.in`/`.oneOf`), see [`@warlock.js/seal/compose-seal-modifiers/SKILL.md`](@warlock.js/seal/compose-seal-modifiers/SKILL.md). For containers (object/array/record/tuple/union), see [`@warlock.js/seal/define-structural-shape/SKILL.md`](@warlock.js/seal/define-structural-shape/SKILL.md).
|
|
142
|
+
|
|
143
|
+
## Things NOT to do
|
|
144
|
+
|
|
145
|
+
- Don't `new ObjectValidator()` (or any class) directly — factory returns carry the StandardSchema bridge that bare instantiation loses.
|
|
146
|
+
- 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.
|
|
147
|
+
- Don't reach for `v.instanceof(Date)` when `v.date()` works. The latter is purpose-built.
|
|
148
|
+
- Don't use `v.string().oneOf(["a", "b"])` for discriminator fields where you need the literal type — use `v.literal("a", "b")`.
|
|
149
|
+
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when *producing* a value.
|