@warlock.js/seal 5.30.0 → 5.31.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 +38 -7
- package/README.md +57 -9
- package/cjs/index.cjs +24 -16
- package/cjs/index.cjs.map +1 -1
- package/cjs/{object--LXRt5RF.cjs → object-VTtA-k1a.cjs} +15 -13
- package/cjs/object-VTtA-k1a.cjs.map +1 -0
- package/cjs/object.cjs +2 -2
- package/cjs/{required-methods-DYsRO9aZ.cjs → required-methods-chT0TXQS.cjs} +107 -31
- package/cjs/required-methods-chT0TXQS.cjs.map +1 -0
- package/cjs/{string-DsqnKoSB.cjs → string-edcPIpcU.cjs} +4 -4
- package/cjs/string-edcPIpcU.cjs.map +1 -0
- package/cjs/string.cjs +2 -2
- package/esm/config.d.mts.map +1 -1
- package/esm/config.mjs.map +1 -1
- package/esm/factory/validators.d.mts +8 -8
- 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/mutators/string-mutators.mjs +2 -2
- package/esm/mutators/string-mutators.mjs.map +1 -1
- package/esm/object.d.mts +1 -3
- package/esm/object.d.mts.map +1 -1
- package/esm/object.mjs.map +1 -1
- package/esm/rules/common/literal.mjs.map +1 -1
- package/esm/rules/core/union.d.mts +8 -1
- 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.mjs.map +1 -1
- 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.map +1 -1
- package/esm/standard-schema/map-result.mjs.map +1 -1
- package/esm/standard-schema/types.d.mts.map +1 -1
- package/esm/types/data-transformer-types.d.mts.map +1 -1
- package/esm/types/inference-types.d.mts +1 -1
- package/esm/types/inference-types.d.mts.map +1 -1
- package/esm/types/rule-types.d.mts +10 -1
- package/esm/types/rule-types.d.mts.map +1 -1
- 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 +8 -8
- package/esm/validators/base-validator.d.mts.map +1 -1
- package/esm/validators/base-validator.mjs +50 -20
- package/esm/validators/base-validator.mjs.map +1 -1
- 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/discriminated-union-validator.mjs +1 -1
- package/esm/validators/discriminated-union-validator.mjs.map +1 -1
- package/esm/validators/lazy-validator.mjs +1 -1
- package/esm/validators/lazy-validator.mjs.map +1 -1
- 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/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.mjs.map +1 -1
- 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 +3 -1
- 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
|
@@ -5,25 +5,25 @@ description: 'Pick the right `v` factory primitive — string / int / literal /
|
|
|
5
5
|
|
|
6
6
|
# Picking the right primitive
|
|
7
7
|
|
|
8
|
-
This is the orientation skill —
|
|
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
9
|
|
|
10
10
|
## Strings
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
v.string()
|
|
14
|
-
v.email()
|
|
15
|
-
v.enum(["a", "b"])
|
|
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
16
|
```
|
|
17
17
|
|
|
18
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
19
|
|
|
20
20
|
## Numbers — pick by what you accept
|
|
21
21
|
|
|
22
|
-
| Validator
|
|
23
|
-
|
|
24
|
-
| `v.number()`
|
|
25
|
-
| `v.int()`
|
|
26
|
-
| `v.float()`
|
|
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
27
|
| `v.numeric()` | numeric strings + numbers | form/query inputs that arrive as `"42"` — coerces to number |
|
|
28
28
|
|
|
29
29
|
All four share the same chain surface (see [`number-methods.md`](./number-methods.md)). Picking is about input acceptance, not chain power.
|
|
@@ -31,8 +31,8 @@ All four share the same chain surface (see [`number-methods.md`](./number-method
|
|
|
31
31
|
## Booleans & scalars
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
|
-
v.boolean()
|
|
35
|
-
v.scalar()
|
|
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
36
|
```
|
|
37
37
|
|
|
38
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.
|
|
@@ -40,8 +40,8 @@ Use `v.scalar()` only when the field truly accepts any of the three primitives.
|
|
|
40
40
|
## Dates
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
|
-
v.date()
|
|
44
|
-
v.instanceof(Date)
|
|
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
45
|
```
|
|
46
46
|
|
|
47
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.
|
|
@@ -49,14 +49,14 @@ v.instanceof(Date) // type: Date — raw instanceof, no normalization, no
|
|
|
49
49
|
## Literals & instances
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
|
-
v.literal("items")
|
|
53
|
-
v.literal("draft", "published", "archived") // type: "draft" | "published" | "archived"
|
|
54
|
-
v.literal(1, 2, 3)
|
|
55
|
-
v.literal(true)
|
|
56
|
-
|
|
57
|
-
v.instanceof(File)
|
|
58
|
-
v.instanceof(Buffer)
|
|
59
|
-
v.instanceof(MyClass)
|
|
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
60
|
```
|
|
61
61
|
|
|
62
62
|
**`v.literal` vs `v.string().oneOf([...])` vs `v.enum([...])`:**
|
|
@@ -65,20 +65,20 @@ v.instanceof(MyClass) // type: MyClass
|
|
|
65
65
|
- `v.string().oneOf(["a", "b"])` infers as `string` (loses literal types). Use when broad type is fine.
|
|
66
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
67
|
|
|
68
|
-
`**`v.literal("")
|
|
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
69
|
|
|
70
70
|
```ts
|
|
71
71
|
const decorative = v.object({ decorative: v.literal(true), alt: v.literal("") });
|
|
72
72
|
|
|
73
|
-
await v.validate(decorative, { decorative: true, alt: "" });
|
|
74
|
-
await v.validate(decorative, { decorative: true, alt: "text" });
|
|
75
|
-
await v.validate(decorative, { decorative: true });
|
|
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
76
|
```
|
|
77
77
|
|
|
78
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
79
|
|
|
80
80
|
:::note[Fixed in 4.9.2]
|
|
81
|
-
Before 4.9.2 `v.literal("")` always failed with
|
|
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
82
|
:::
|
|
83
83
|
|
|
84
84
|
:::note[Fixed again in 5.30]
|
|
@@ -90,7 +90,7 @@ Before 4.9.2 `v.literal("")` always failed with *"is required"*, and `.optional(
|
|
|
90
90
|
## `v.any` — escape hatch
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
|
-
v.any()
|
|
93
|
+
v.any(); // type: any — skips validation entirely
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
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.
|
|
@@ -112,26 +112,26 @@ v.object({
|
|
|
112
112
|
- **`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))`.
|
|
113
113
|
- **`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.
|
|
114
114
|
|
|
115
|
-
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()`
|
|
115
|
+
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).
|
|
116
116
|
|
|
117
117
|
## Quick map — "I need to validate…"
|
|
118
118
|
|
|
119
|
-
| Need
|
|
120
|
-
|
|
121
|
-
| Email
|
|
122
|
-
| URL
|
|
123
|
-
| UUID
|
|
124
|
-
| Number 0–100
|
|
125
|
-
| Positive integer
|
|
126
|
-
| One of N constants
|
|
127
|
-
| One of TS enum values
|
|
128
|
-
| Date in the past
|
|
129
|
-
| File upload
|
|
130
|
-
| Class instance (not Date)
|
|
131
|
-
| Discriminated union
|
|
132
|
-
| Derived value (computed from siblings) | `v.computed<T>(callback)`
|
|
133
|
-
| Framework-injected value
|
|
134
|
-
| Free-form / pass-through
|
|
119
|
+
| Need | Reach for |
|
|
120
|
+
| -------------------------------------- | -------------------------------------------------------------------- |
|
|
121
|
+
| Email | `v.string().email()` or `v.email()` |
|
|
122
|
+
| URL | `v.string().url()` |
|
|
123
|
+
| UUID | `v.string().uuid()` (see [`string-methods.md`](./string-methods.md)) |
|
|
124
|
+
| Number 0–100 | `v.number().between(0, 100)` |
|
|
125
|
+
| Positive integer | `v.int().positive()` |
|
|
126
|
+
| One of N constants | `v.literal(...values)` |
|
|
127
|
+
| One of TS enum values | `v.enum(MyEnum)` |
|
|
128
|
+
| Date in the past | `v.date().past()` |
|
|
129
|
+
| File upload | `v.instanceof(File)` |
|
|
130
|
+
| Class instance (not Date) | `v.instanceof(Ctor)` |
|
|
131
|
+
| Discriminated union | see the `define-structural-shape` topic |
|
|
132
|
+
| Derived value (computed from siblings) | `v.computed<T>(callback)` |
|
|
133
|
+
| Framework-injected value | `v.managed<T>(callback)` |
|
|
134
|
+
| Free-form / pass-through | `v.any()` (only when you've thought about it) |
|
|
135
135
|
|
|
136
136
|
## Method-surface reference
|
|
137
137
|
|
|
@@ -150,4 +150,4 @@ For cross-cutting modifiers (`.optional`/`.nullable`/`.default`/`.catch`/`.in`/`
|
|
|
150
150
|
- 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.
|
|
151
151
|
- Don't reach for `v.instanceof(Date)` when `v.date()` works. The latter is purpose-built.
|
|
152
152
|
- Don't use `v.string().oneOf(["a", "b"])` for discriminator fields where you need the literal type — use `v.literal("a", "b")`.
|
|
153
|
-
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when
|
|
153
|
+
- Don't expect `v.computed` / `v.managed` to validate input — they ignore input shape entirely. Reach for them only when _producing_ a value.
|
|
@@ -4,139 +4,139 @@
|
|
|
4
4
|
|
|
5
5
|
## Range — global value comparison
|
|
6
6
|
|
|
7
|
-
| Method
|
|
8
|
-
|
|
9
|
-
| `.min(dateOrField, msg?)`
|
|
10
|
-
| `.max(dateOrField, msg?)`
|
|
11
|
-
| `.before(dateOrField, msg?)` | strict `<`
|
|
12
|
-
| `.after(dateOrField, msg?)`
|
|
13
|
-
| `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)`
|
|
7
|
+
| Method | Args | Example |
|
|
8
|
+
| ---------------------------- | --------------- | ---------------------------------------------------------- |
|
|
9
|
+
| `.min(dateOrField, msg?)` | inclusive `>=` | `v.date().min("2024-01-01")` or `v.date().min(new Date())` |
|
|
10
|
+
| `.max(dateOrField, msg?)` | inclusive `<=` | `v.date().max(new Date())` |
|
|
11
|
+
| `.before(dateOrField, msg?)` | strict `<` | `v.date().before(new Date())` |
|
|
12
|
+
| `.after(dateOrField, msg?)` | strict `>` | `v.date().after(new Date())` |
|
|
13
|
+
| `.between(start, end, msg?)` | inclusive range | `v.date().between(start, end)` |
|
|
14
14
|
|
|
15
15
|
Smart detection: a string with `-` or `/` is a date string; a plain string is a sibling field name.
|
|
16
16
|
|
|
17
17
|
## Range — explicit sibling scope
|
|
18
18
|
|
|
19
|
-
| Method
|
|
20
|
-
|
|
21
|
-
| `.minSibling(field, msg?)`
|
|
22
|
-
| `.maxSibling(field, msg?)`
|
|
23
|
-
| `.beforeSibling(field, msg?)`
|
|
24
|
-
| `.afterSibling(field, msg?)`
|
|
25
|
-
| `.sameAsField(field, msg?)`
|
|
19
|
+
| Method | Effect |
|
|
20
|
+
| ---------------------------------- | ------------------------------------ |
|
|
21
|
+
| `.minSibling(field, msg?)` | `>=` sibling field |
|
|
22
|
+
| `.maxSibling(field, msg?)` | `<=` sibling field |
|
|
23
|
+
| `.beforeSibling(field, msg?)` | `<` sibling field |
|
|
24
|
+
| `.afterSibling(field, msg?)` | `>` sibling field |
|
|
25
|
+
| `.sameAsField(field, msg?)` | `===` sibling field |
|
|
26
26
|
| `.sameAsFieldSibling(field, msg?)` | `===` sibling field (explicit scope) |
|
|
27
27
|
|
|
28
28
|
Only run inside `v.object`. Not representable in JSON Schema.
|
|
29
29
|
|
|
30
30
|
## Today / past / future
|
|
31
31
|
|
|
32
|
-
| Method
|
|
33
|
-
|
|
34
|
-
| `.today(msg?)`
|
|
35
|
-
| `.fromToday(msg?)`
|
|
32
|
+
| Method | Effect |
|
|
33
|
+
| -------------------- | --------------------- |
|
|
34
|
+
| `.today(msg?)` | exactly today |
|
|
35
|
+
| `.fromToday(msg?)` | today or future |
|
|
36
36
|
| `.beforeToday(msg?)` | strictly before today |
|
|
37
|
-
| `.afterToday(msg?)`
|
|
38
|
-
| `.past(msg?)`
|
|
39
|
-
| `.future(msg?)`
|
|
37
|
+
| `.afterToday(msg?)` | strictly after today |
|
|
38
|
+
| `.past(msg?)` | any past date |
|
|
39
|
+
| `.future(msg?)` | any future date |
|
|
40
40
|
|
|
41
41
|
## Relative window
|
|
42
42
|
|
|
43
|
-
| Method
|
|
44
|
-
|
|
45
|
-
| `.withinDays(n, msg?)`
|
|
46
|
-
| `.withinPastDays(n, msg?)`
|
|
47
|
-
| `.withinFutureDays(n, msg?)` | within N days in the future
|
|
43
|
+
| Method | Args | Effect |
|
|
44
|
+
| ---------------------------- | ---------------------------- | ------ |
|
|
45
|
+
| `.withinDays(n, msg?)` | within N days past or future | — |
|
|
46
|
+
| `.withinPastDays(n, msg?)` | within N days in the past | — |
|
|
47
|
+
| `.withinFutureDays(n, msg?)` | within N days in the future | — |
|
|
48
48
|
|
|
49
49
|
## Age
|
|
50
50
|
|
|
51
|
-
| Method
|
|
52
|
-
|
|
53
|
-
| `.age(years, msg?)`
|
|
54
|
-
| `.minAge(years, msg?)`
|
|
55
|
-
| `.maxAge(years, msg?)`
|
|
56
|
-
| `.betweenAge(min, max, msg?)`
|
|
51
|
+
| Method | Args | Effect |
|
|
52
|
+
| ----------------------------------- | --------------------------------- | ------ |
|
|
53
|
+
| `.age(years, msg?)` | exactly N years old |
|
|
54
|
+
| `.minAge(years, msg?)` | at least N years old |
|
|
55
|
+
| `.maxAge(years, msg?)` | at most N years old |
|
|
56
|
+
| `.betweenAge(min, max, msg?)` | between min/max years |
|
|
57
57
|
| `.birthday(minAge?, maxAge?, msg?)` | not in future, optional age range |
|
|
58
58
|
|
|
59
59
|
## Weekday / weekend / business day
|
|
60
60
|
|
|
61
|
-
| Method
|
|
62
|
-
|
|
63
|
-
| `.weekDay(day, msg?)`
|
|
64
|
-
| `.weekdays(days, msg?)` | array of weekdays
|
|
65
|
-
| `.weekend(msg?)`
|
|
66
|
-
| `.businessDay(msg?)`
|
|
61
|
+
| Method | Args | Effect |
|
|
62
|
+
| ----------------------- | ----------------------------- | ------ |
|
|
63
|
+
| `.weekDay(day, msg?)` | day = `"monday"` … `"sunday"` |
|
|
64
|
+
| `.weekdays(days, msg?)` | array of weekdays |
|
|
65
|
+
| `.weekend(msg?)` | Saturday or Sunday |
|
|
66
|
+
| `.businessDay(msg?)` | Monday – Friday |
|
|
67
67
|
|
|
68
68
|
## Period — month / year / quarter
|
|
69
69
|
|
|
70
|
-
| Method
|
|
71
|
-
|
|
72
|
-
| `.month(m, msg?)`
|
|
73
|
-
| `.year(y, msg?)`
|
|
74
|
-
| `.quarter(q, msg?)`
|
|
75
|
-
| `.leapYear(msg?)`
|
|
76
|
-
| `.minYear(yearOrField, msg?)`
|
|
77
|
-
| `.maxYear(yearOrField, msg?)`
|
|
78
|
-
| `.minMonth(mOrField, msg?)`
|
|
79
|
-
| `.maxMonth(mOrField, msg?)`
|
|
80
|
-
| `.minDay(dOrField, msg?)`
|
|
81
|
-
| `.maxDay(dOrField, msg?)`
|
|
82
|
-
| `.betweenYears(start, end, msg?)`
|
|
83
|
-
| `.betweenMonths(start, end, msg?)` | inclusive month range
|
|
84
|
-
| `.betweenDays(start, end, msg?)`
|
|
70
|
+
| Method | Args | Effect |
|
|
71
|
+
| ---------------------------------- | ---------------------------- | ------ |
|
|
72
|
+
| `.month(m, msg?)` | `m` = 1–12 (or `Month` enum) |
|
|
73
|
+
| `.year(y, msg?)` | exact year |
|
|
74
|
+
| `.quarter(q, msg?)` | `q` = 1–4 |
|
|
75
|
+
| `.leapYear(msg?)` | year is a leap year |
|
|
76
|
+
| `.minYear(yearOrField, msg?)` | year `>=` |
|
|
77
|
+
| `.maxYear(yearOrField, msg?)` | year `<=` |
|
|
78
|
+
| `.minMonth(mOrField, msg?)` | month `>=` |
|
|
79
|
+
| `.maxMonth(mOrField, msg?)` | month `<=` |
|
|
80
|
+
| `.minDay(dOrField, msg?)` | day-of-month `>=` |
|
|
81
|
+
| `.maxDay(dOrField, msg?)` | day-of-month `<=` |
|
|
82
|
+
| `.betweenYears(start, end, msg?)` | inclusive year range |
|
|
83
|
+
| `.betweenMonths(start, end, msg?)` | inclusive month range |
|
|
84
|
+
| `.betweenDays(start, end, msg?)` | inclusive day-of-month range |
|
|
85
85
|
|
|
86
86
|
Each `min*` / `max*` / `between*` accepts a sibling field name. Sibling-explicit variants exist: `.minYearSibling`, `.maxYearSibling`, `.minMonthSibling`, `.maxMonthSibling`, `.minDaySibling`, `.maxDaySibling`, `.betweenYearsSibling`, `.betweenMonthsSibling`, `.betweenDaysSibling`.
|
|
87
87
|
|
|
88
88
|
## Time — hour / minute
|
|
89
89
|
|
|
90
|
-
| Method
|
|
91
|
-
|
|
92
|
-
| `.fromHour(h, msg?)`
|
|
93
|
-
| `.beforeHour(h, msg?)`
|
|
94
|
-
| `.betweenHours(start, end, msg?)`
|
|
95
|
-
| `.fromMinute(m, msg?)`
|
|
96
|
-
| `.beforeMinute(m, msg?)`
|
|
97
|
-
| `.betweenMinutes(start, end, msg?)` | inclusive minute range
|
|
98
|
-
| `.betweenTimes(start, end, msg?)`
|
|
90
|
+
| Method | Args | Effect |
|
|
91
|
+
| ----------------------------------- | ------------------------ | ------ |
|
|
92
|
+
| `.fromHour(h, msg?)` | h = 0–23, time `>= h:00` |
|
|
93
|
+
| `.beforeHour(h, msg?)` | time `< h:00` |
|
|
94
|
+
| `.betweenHours(start, end, msg?)` | inclusive hour range |
|
|
95
|
+
| `.fromMinute(m, msg?)` | m = 0–59 |
|
|
96
|
+
| `.beforeMinute(m, msg?)` | — |
|
|
97
|
+
| `.betweenMinutes(start, end, msg?)` | inclusive minute range |
|
|
98
|
+
| `.betweenTimes(start, end, msg?)` | "HH:MM" strings |
|
|
99
99
|
|
|
100
100
|
## Format
|
|
101
101
|
|
|
102
|
-
| Method
|
|
103
|
-
|
|
102
|
+
| Method | Args | Effect |
|
|
103
|
+
| -------------------- | ------------------- | --------------------------- |
|
|
104
104
|
| `.format(fmt, msg?)` | dayjs format string | input must match the format |
|
|
105
105
|
|
|
106
106
|
## Mutators (pre-validation reshape)
|
|
107
107
|
|
|
108
|
-
| Method
|
|
109
|
-
|
|
110
|
-
| `.toStartOfDay()`
|
|
111
|
-
| `.toEndOfDay()`
|
|
112
|
-
| `.toStartOfMonth()` | first day of month
|
|
113
|
-
| `.toEndOfMonth()`
|
|
114
|
-
| `.toStartOfYear()`
|
|
115
|
-
| `.toEndOfYear()`
|
|
116
|
-
| `.addDays(n)`
|
|
117
|
-
| `.addMonths(n)`
|
|
118
|
-
| `.addYears(n)`
|
|
119
|
-
| `.addHours(n)`
|
|
120
|
-
| `.toUTC()`
|
|
121
|
-
|
|
122
|
-
Mutators run
|
|
108
|
+
| Method | Effect |
|
|
109
|
+
| ------------------- | --------------------------------- |
|
|
110
|
+
| `.toStartOfDay()` | 00:00:00.000 |
|
|
111
|
+
| `.toEndOfDay()` | 23:59:59.999 |
|
|
112
|
+
| `.toStartOfMonth()` | first day of month |
|
|
113
|
+
| `.toEndOfMonth()` | last day of month |
|
|
114
|
+
| `.toStartOfYear()` | January 1st |
|
|
115
|
+
| `.toEndOfYear()` | December 31st |
|
|
116
|
+
| `.addDays(n)` | shift by N days (negative = back) |
|
|
117
|
+
| `.addMonths(n)` | shift by N months |
|
|
118
|
+
| `.addYears(n)` | shift by N years |
|
|
119
|
+
| `.addHours(n)` | shift by N hours |
|
|
120
|
+
| `.toUTC()` | normalize to UTC |
|
|
121
|
+
|
|
122
|
+
Mutators run _before_ rules. `v.date().addDays(7).future()` checks whether the shifted date is in the future.
|
|
123
123
|
|
|
124
124
|
## Transformers (post-validation, reshape `data`)
|
|
125
125
|
|
|
126
|
-
| Method
|
|
127
|
-
|
|
128
|
-
| `.toISOString()` | —
|
|
129
|
-
| `.toTimestamp()` | —
|
|
130
|
-
| `.toFormat(fmt)` | dayjs format string | `Date` → formatted string
|
|
131
|
-
| `.toDateOnly()`
|
|
132
|
-
| `.toTimeOnly()`
|
|
126
|
+
| Method | Args | Effect |
|
|
127
|
+
| ---------------- | ------------------- | ------------------------------------- |
|
|
128
|
+
| `.toISOString()` | — | `Date` → `"2026-01-15T00:00:00.000Z"` |
|
|
129
|
+
| `.toTimestamp()` | — | `Date` → number (ms since epoch) |
|
|
130
|
+
| `.toFormat(fmt)` | dayjs format string | `Date` → formatted string |
|
|
131
|
+
| `.toDateOnly()` | — | `"YYYY-MM-DD"` |
|
|
132
|
+
| `.toTimeOnly()` | — | `"HH:mm:ss"` |
|
|
133
133
|
|
|
134
|
-
Transformers shape the
|
|
134
|
+
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.
|
|
135
135
|
|
|
136
136
|
## Defaults
|
|
137
137
|
|
|
138
|
-
| Method
|
|
139
|
-
|
|
138
|
+
| Method | Effect |
|
|
139
|
+
| --------------- | ------------------------------------------ |
|
|
140
140
|
| `.defaultNow()` | shorthand for `.default(() => new Date())` |
|
|
141
141
|
|
|
142
142
|
## JSON Schema mapping
|
|
@@ -150,26 +150,26 @@ Transformers shape the *output*. `Infer<>` still resolves to `Date` even if a tr
|
|
|
150
150
|
|
|
151
151
|
```ts
|
|
152
152
|
// Birthday — must be 13+ and in the past
|
|
153
|
-
v.date().past().minAge(13)
|
|
153
|
+
v.date().past().minAge(13);
|
|
154
154
|
|
|
155
155
|
// Reservation — future, business day, between hours
|
|
156
|
-
v.date().future().businessDay().betweenHours(9, 17)
|
|
156
|
+
v.date().future().businessDay().betweenHours(9, 17);
|
|
157
157
|
|
|
158
158
|
// Effective date range (cross-field)
|
|
159
159
|
v.object({
|
|
160
160
|
startsAt: v.date(),
|
|
161
161
|
endsAt: v.date().afterSibling("startsAt"),
|
|
162
|
-
})
|
|
162
|
+
});
|
|
163
163
|
|
|
164
164
|
// API response — emit ISO string
|
|
165
|
-
v.date().toISOString()
|
|
165
|
+
v.date().toISOString();
|
|
166
166
|
|
|
167
167
|
// Event window — within 30 future days, normalized to UTC
|
|
168
|
-
v.date().toUTC().withinFutureDays(30)
|
|
168
|
+
v.date().toUTC().withinFutureDays(30);
|
|
169
169
|
|
|
170
170
|
// Quarterly report
|
|
171
|
-
v.date().quarter(1).year(2026)
|
|
171
|
+
v.date().quarter(1).year(2026);
|
|
172
172
|
|
|
173
173
|
// Date-only, end-of-day for inclusive comparisons
|
|
174
|
-
v.date().toEndOfDay().toDateOnly()
|
|
174
|
+
v.date().toEndOfDay().toDateOnly();
|
|
175
175
|
```
|
|
@@ -4,23 +4,23 @@ All four share this surface. The picking guide (which factory to call) is in the
|
|
|
4
4
|
|
|
5
5
|
## Range — global value comparison
|
|
6
6
|
|
|
7
|
-
| Method
|
|
8
|
-
|
|
9
|
-
| `.min(n, msg?)`
|
|
10
|
-
| `.max(n, msg?)`
|
|
11
|
-
| `.between(a, b, msg?)`
|
|
12
|
-
| `.greaterThan(n, msg?)` | strict `>`
|
|
13
|
-
| `.gt(n, msg?)`
|
|
14
|
-
| `.lessThan(n, msg?)`
|
|
15
|
-
| `.lt(n, msg?)`
|
|
7
|
+
| Method | Args | JSON Schema | Example |
|
|
8
|
+
| ----------------------- | ------------------------ | ------------------------ | -------------------------- |
|
|
9
|
+
| `.min(n, msg?)` | inclusive lower bound | `minimum: n` | `v.int().min(0)` |
|
|
10
|
+
| `.max(n, msg?)` | inclusive upper bound | `maximum: n` | `v.int().max(100)` |
|
|
11
|
+
| `.between(a, b, msg?)` | inclusive range | `minimum: a, maximum: b` | `v.number().between(0, 1)` |
|
|
12
|
+
| `.greaterThan(n, msg?)` | strict `>` | `exclusiveMinimum: n` | `v.int().greaterThan(0)` |
|
|
13
|
+
| `.gt(n, msg?)` | alias for `.greaterThan` | `exclusiveMinimum: n` | — |
|
|
14
|
+
| `.lessThan(n, msg?)` | strict `<` | `exclusiveMaximum: n` | `v.int().lessThan(100)` |
|
|
15
|
+
| `.lt(n, msg?)` | alias for `.lessThan` | `exclusiveMaximum: n` | — |
|
|
16
16
|
|
|
17
17
|
`.min` / `.max` / `.between` / `.greaterThan` / `.lessThan` accept a **string** as the value — interpreted as a sibling field name (smart detection):
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
20
|
v.object({
|
|
21
21
|
minPrice: v.int(),
|
|
22
|
-
maxPrice: v.int().min("minPrice"),
|
|
23
|
-
})
|
|
22
|
+
maxPrice: v.int().min("minPrice"), // maxPrice >= minPrice
|
|
23
|
+
});
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Sibling references are **not representable in JSON Schema** — silently omitted from generated output.
|
|
@@ -29,68 +29,68 @@ Sibling references are **not representable in JSON Schema** — silently omitted
|
|
|
29
29
|
|
|
30
30
|
For when smart detection is ambiguous (e.g. a numeric string that happens to match a field name):
|
|
31
31
|
|
|
32
|
-
| Method
|
|
33
|
-
|
|
34
|
-
| `.minSibling(field, msg?)`
|
|
35
|
-
| `.maxSibling(field, msg?)`
|
|
36
|
-
| `.greaterThanSibling(field, msg?)`
|
|
37
|
-
| `.gtSibling(field, msg?)`
|
|
38
|
-
| `.lessThanSibling(field, msg?)`
|
|
39
|
-
| `.ltSibling(field, msg?)`
|
|
32
|
+
| Method | Effect |
|
|
33
|
+
| ------------------------------------------- | -------------------------- |
|
|
34
|
+
| `.minSibling(field, msg?)` | value `>=` sibling field |
|
|
35
|
+
| `.maxSibling(field, msg?)` | value `<=` sibling field |
|
|
36
|
+
| `.greaterThanSibling(field, msg?)` | value `>` sibling field |
|
|
37
|
+
| `.gtSibling(field, msg?)` | alias |
|
|
38
|
+
| `.lessThanSibling(field, msg?)` | value `<` sibling field |
|
|
39
|
+
| `.ltSibling(field, msg?)` | alias |
|
|
40
40
|
| `.betweenSibling(minField, maxField, msg?)` | between two sibling fields |
|
|
41
41
|
|
|
42
42
|
These **only run inside `v.object`** — sibling resolution silently passes otherwise.
|
|
43
43
|
|
|
44
44
|
## Sign & parity
|
|
45
45
|
|
|
46
|
-
| Method
|
|
47
|
-
|
|
48
|
-
| `.positive(msg?)` | value `> 0`
|
|
49
|
-
| `.negative(msg?)` | value `< 0`
|
|
50
|
-
| `.odd(msg?)`
|
|
51
|
-
| `.even(msg?)`
|
|
46
|
+
| Method | Effect | Example |
|
|
47
|
+
| ----------------- | ------------- | -------------------- |
|
|
48
|
+
| `.positive(msg?)` | value `> 0` | `v.int().positive()` |
|
|
49
|
+
| `.negative(msg?)` | value `< 0` | `v.int().negative()` |
|
|
50
|
+
| `.odd(msg?)` | value is odd | `v.int().odd()` |
|
|
51
|
+
| `.even(msg?)` | value is even | `v.int().even()` |
|
|
52
52
|
|
|
53
53
|
## Divisibility / modulo
|
|
54
54
|
|
|
55
|
-
| Method
|
|
56
|
-
|
|
57
|
-
| `.modulo(n, msg?)`
|
|
58
|
-
| `.divisibleBy(n, msg?)` | alias
|
|
59
|
-
| `.multipleOf(n, msg?)`
|
|
60
|
-
| `.modulusOf(n, msg?)`
|
|
55
|
+
| Method | Effect | JSON Schema |
|
|
56
|
+
| ----------------------- | ----------------- | --------------- |
|
|
57
|
+
| `.modulo(n, msg?)` | value `% n === 0` | `multipleOf: n` |
|
|
58
|
+
| `.divisibleBy(n, msg?)` | alias | `multipleOf: n` |
|
|
59
|
+
| `.multipleOf(n, msg?)` | alias | `multipleOf: n` |
|
|
60
|
+
| `.modulusOf(n, msg?)` | alias | `multipleOf: n` |
|
|
61
61
|
|
|
62
62
|
## String-form length (rare)
|
|
63
63
|
|
|
64
64
|
When the numeric value is a fixed-format code (PINs, IDs):
|
|
65
65
|
|
|
66
|
-
| Method
|
|
67
|
-
|
|
68
|
-
| `.length(n, msg?)`
|
|
69
|
-
| `.minLength(n, msg?)` | string-rep length `>= n`
|
|
70
|
-
| `.maxLength(n, msg?)` | string-rep length `<= n`
|
|
66
|
+
| Method | Effect |
|
|
67
|
+
| --------------------- | ------------------------------------- |
|
|
68
|
+
| `.length(n, msg?)` | string-rep length must be exactly `n` |
|
|
69
|
+
| `.minLength(n, msg?)` | string-rep length `>= n` |
|
|
70
|
+
| `.maxLength(n, msg?)` | string-rep length `<= n` |
|
|
71
71
|
|
|
72
72
|
For most numeric-shaped IDs you'd use `v.string().length(n).numeric()` instead — let the value be a string.
|
|
73
73
|
|
|
74
74
|
## Mutators (pre-validation reshape)
|
|
75
75
|
|
|
76
|
-
| Method
|
|
77
|
-
|
|
78
|
-
| `.abs()`
|
|
79
|
-
| `.ceil()`
|
|
80
|
-
| `.floor()`
|
|
81
|
-
| `.round(decimals?)`
|
|
82
|
-
| `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number**
|
|
83
|
-
| `.coerce()`
|
|
76
|
+
| Method | Args | Effect |
|
|
77
|
+
| --------------------- | --------- | -------------------------------------------------------------------------------- |
|
|
78
|
+
| `.abs()` | — | `Math.abs(value)` |
|
|
79
|
+
| `.ceil()` | — | round up to integer |
|
|
80
|
+
| `.floor()` | — | round down to integer |
|
|
81
|
+
| `.round(decimals?)` | default 0 | round to N decimals |
|
|
82
|
+
| `.toFixed(decimals?)` | default 2 | round to N decimals — returns a **number** |
|
|
83
|
+
| `.coerce()` | — | a **numeric-shaped string** → `Number`; any other value passes through unchanged |
|
|
84
84
|
|
|
85
|
-
These run
|
|
85
|
+
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.
|
|
86
86
|
|
|
87
87
|
`.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`:
|
|
88
88
|
|
|
89
89
|
```ts
|
|
90
|
-
v.object({ limit: v.int().coerce().min(1).max(100) })
|
|
90
|
+
v.object({ limit: v.int().coerce().min(1).max(100) }); // "2" → 2, passes
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
It only fixes the string
|
|
93
|
+
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.
|
|
94
94
|
|
|
95
95
|
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.
|
|
96
96
|
|
|
@@ -110,29 +110,29 @@ Before 4.9.2 `.toFixed()` returned a string, so `v.number().toFixed(2)` **always
|
|
|
110
110
|
|
|
111
111
|
```ts
|
|
112
112
|
// Age
|
|
113
|
-
v.int().min(0).max(150)
|
|
113
|
+
v.int().min(0).max(150);
|
|
114
114
|
|
|
115
115
|
// Price (cents)
|
|
116
|
-
v.int().min(0)
|
|
116
|
+
v.int().min(0);
|
|
117
117
|
|
|
118
118
|
// Probability
|
|
119
|
-
v.number().between(0, 1)
|
|
119
|
+
v.number().between(0, 1);
|
|
120
120
|
|
|
121
121
|
// Even page index
|
|
122
|
-
v.int().min(0).even()
|
|
122
|
+
v.int().min(0).even();
|
|
123
123
|
|
|
124
124
|
// Quantity divisible by box size
|
|
125
|
-
v.int().min(1).multipleOf(12)
|
|
125
|
+
v.int().min(1).multipleOf(12);
|
|
126
126
|
|
|
127
127
|
// Coerced from form input
|
|
128
|
-
v.numeric().min(0).max(100)
|
|
128
|
+
v.numeric().min(0).max(100);
|
|
129
129
|
|
|
130
130
|
// Cross-field range
|
|
131
131
|
v.object({
|
|
132
132
|
startYear: v.int(),
|
|
133
133
|
endYear: v.int().minSibling("startYear"),
|
|
134
|
-
})
|
|
134
|
+
});
|
|
135
135
|
|
|
136
136
|
// Optional with default
|
|
137
|
-
v.int().min(0).default(0).optional()
|
|
137
|
+
v.int().min(0).default(0).optional();
|
|
138
138
|
```
|