@warlock.js/seal 5.23.2 → 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 +17 -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.mjs.map +1 -1
- package/esm/factory/validate.mjs.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.mjs.map +1 -1
- package/esm/helpers/path-helpers.mjs.map +1 -1
- package/esm/mutators/array-mutators.mjs.map +1 -1
- package/esm/mutators/date-mutators.mjs.map +1 -1
- package/esm/mutators/number-mutators.mjs.map +1 -1
- package/esm/mutators/object-mutators.mjs.map +1 -1
- package/esm/mutators/scalar-mutators.mjs.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.mjs.map +1 -1
- package/esm/rules/color/color-rules.mjs.map +1 -1
- package/esm/rules/common/enum.mjs.map +1 -1
- package/esm/rules/common/equals-field-rules.mjs.map +1 -1
- package/esm/rules/common/type-rules.mjs.map +1 -1
- package/esm/rules/conditional/forbidden-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-unless-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-with-rules.mjs.map +1 -1
- package/esm/rules/conditional/present-without-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-if-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-unless-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-with-rules.mjs.map +1 -1
- package/esm/rules/conditional/required-without-rules.mjs.map +1 -1
- package/esm/rules/core/equal.mjs.map +1 -1
- package/esm/rules/core/forbidden.mjs.map +1 -1
- package/esm/rules/core/required.mjs.map +1 -1
- package/esm/rules/core/union.mjs.map +1 -1
- package/esm/rules/core/when.mjs.map +1 -1
- package/esm/rules/date/date-comparison-rules.mjs.map +1 -1
- package/esm/rules/date/date-day-rules.mjs.map +1 -1
- package/esm/rules/date/date-field-comparison-rules.mjs.map +1 -1
- package/esm/rules/date/date-period-rules.mjs.map +1 -1
- package/esm/rules/date/date-relative-rules.mjs.map +1 -1
- package/esm/rules/date/date-special-rules.mjs.map +1 -1
- package/esm/rules/date/date.mjs.map +1 -1
- package/esm/rules/file/dimensions.mjs.map +1 -1
- package/esm/rules/file/file-size.mjs.map +1 -1
- package/esm/rules/length/length-rules.mjs.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.mjs.map +1 -1
- package/esm/rules/string/credit-card.mjs.map +1 -1
- package/esm/rules/string/email.mjs.map +1 -1
- package/esm/rules/string/matches.mjs.map +1 -1
- package/esm/rules/string/pattern.mjs.map +1 -1
- package/esm/rules/string/string-comparison.mjs.map +1 -1
- package/esm/rules/string/strong-password-rule.mjs.map +1 -1
- package/esm/rules/string/url.mjs.map +1 -1
- package/esm/rules/string/without-whitespace.mjs.map +1 -1
- package/esm/types/date-types.mjs.map +1 -1
- 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 -0
- 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/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/float-validator.mjs.map +1 -1
- package/esm/validators/int-validator.mjs.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/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.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.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.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,178 +1,178 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: extend-seal-with-plugins
|
|
3
|
-
description: 'Author a custom seal plugin to add validator methods (`.slug`, `.postalCode`, etc) — `SealPlugin` shape, `registerPlugin` lifecycle, TS prototype augmentation. Triggers: `SealPlugin`, `registerPlugin`, `unregisterPlugin`, `hasPlugin`, `getInstalledPlugins`, `StringValidator`, `NumberValidator`, `DateValidator`, `BaseValidator`, `install`, `uninstall`; "how do I add a custom validator method", "write a seal plugin", "extend v.string with .slug", "module augmentation for seal"; typical import `import { StringValidator, registerPlugin, type SealPlugin } from "@warlock.js/seal"`. Skip: built-in primitives — `@warlock.js/seal/pick-seal-primitive/SKILL.md`; bridge typing — `@warlock.js/seal/bridge-standard-schema/SKILL.md`; competing `zod` `.refine`/`.transform`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Extend seal with plugins
|
|
7
|
-
|
|
8
|
-
Seal exposes a plugin system so you can add validator methods without forking the package. A plugin is an object with `name`, optional `version`/`description`, and an `install` function that runs once when the plugin is registered. The standard pattern is to `Object.assign(StringValidator.prototype, { ... })` to graft new methods onto a validator class.
|
|
9
|
-
|
|
10
|
-
## When to reach for a plugin
|
|
11
|
-
|
|
12
|
-
- The validator method you want **does not exist** in built-in seal — check [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md) and its method references first.
|
|
13
|
-
- The validation is **stable and reusable** across modules — domain-specific formats (IBAN, postal codes, tax IDs, license plates, internal ID schemes).
|
|
14
|
-
- You want the **chainable syntax** — `v.string().slug()` reads better than `v.string().pattern(/.../).addMutator(s => slugify(s))` at every call site.
|
|
15
|
-
|
|
16
|
-
**Don't** reach for a plugin when a one-off `.pattern()` would do. The boilerplate (declare module, register on boot) is justified only when you'll call the new method many times.
|
|
17
|
-
|
|
18
|
-
## The plugin shape
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import type { SealPlugin } from "@warlock.js/seal";
|
|
22
|
-
|
|
23
|
-
type SealPlugin = {
|
|
24
|
-
name: string; // unique identifier — duplicates warn and skip install
|
|
25
|
-
version?: string;
|
|
26
|
-
description?: string;
|
|
27
|
-
install: (context: { name: string; version?: string }) => void | Promise<void>;
|
|
28
|
-
uninstall?: () => void | Promise<void>;
|
|
29
|
-
};
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
The `install` function is where you add methods. Typically you patch a validator class prototype:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
36
|
-
|
|
37
|
-
export const slugPlugin: SealPlugin = {
|
|
38
|
-
name: "slug",
|
|
39
|
-
version: "1.0.0",
|
|
40
|
-
description: "Adds .slug() — pattern-only slug validation",
|
|
41
|
-
|
|
42
|
-
install() {
|
|
43
|
-
Object.assign(StringValidator.prototype, {
|
|
44
|
-
slug(this: StringValidator, errorMessage?: string) {
|
|
45
|
-
return this.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, errorMessage);
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`uninstall` is optional. Provide it when your plugin needs to clean up — e.g. removing prototype methods for hot-reload scenarios. Most production plugins skip it; methods grafted at boot stay for the process lifetime.
|
|
53
|
-
|
|
54
|
-
## Registering plugins
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
import { registerPlugin, unregisterPlugin, hasPlugin, getInstalledPlugins } from "@warlock.js/seal";
|
|
58
|
-
|
|
59
|
-
await registerPlugin(slugPlugin);
|
|
60
|
-
// console.warn if "slug" is already installed; otherwise install() runs and the plugin is tracked.
|
|
61
|
-
|
|
62
|
-
hasPlugin("slug"); // → true
|
|
63
|
-
getInstalledPlugins(); // → [slugPlugin]
|
|
64
|
-
|
|
65
|
-
await unregisterPlugin("slug");
|
|
66
|
-
// runs slugPlugin.uninstall?.(); removes from registry.
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
`registerPlugin` is async (the `install` function may be async). Await it during boot so the methods are available before the first request.
|
|
70
|
-
|
|
71
|
-
**Where to register:** the conventional place in a Warlock app is a side-effect file loaded by `warlock.config.ts` — e.g. `src/setup/seal-plugins.ts`:
|
|
72
|
-
|
|
73
|
-
```ts title="src/setup/seal-plugins.ts"
|
|
74
|
-
import { registerPlugin } from "@warlock.js/seal";
|
|
75
|
-
import { slugPlugin } from "./plugins/slug-plugin";
|
|
76
|
-
import { postalCodePlugin } from "./plugins/postal-code-plugin";
|
|
77
|
-
|
|
78
|
-
export async function setupSealPlugins() {
|
|
79
|
-
await registerPlugin(slugPlugin);
|
|
80
|
-
await registerPlugin(postalCodePlugin);
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Then call `setupSealPlugins()` in a bootstrap connector. Registering at top-level module scope works too (it's idempotent — duplicates warn and skip), but explicit setup is clearer.
|
|
85
|
-
|
|
86
|
-
## TypeScript — declare the new methods
|
|
87
|
-
|
|
88
|
-
`Object.assign` on a prototype is invisible to TypeScript. Declare the new methods with module augmentation so call sites compile:
|
|
89
|
-
|
|
90
|
-
```ts title="src/setup/seal-plugins.types.ts"
|
|
91
|
-
import "@warlock.js/seal";
|
|
92
|
-
|
|
93
|
-
declare module "@warlock.js/seal" {
|
|
94
|
-
interface StringValidator {
|
|
95
|
-
/** Pattern-only slug — `"hello-world"`, not `"Hello World"`. */
|
|
96
|
-
slug(errorMessage?: string): StringValidator;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
After this file is included in the project's `tsconfig.json` (via `include` or a side-effect import), `v.string().slug()` autocompletes and type-checks everywhere.
|
|
102
|
-
|
|
103
|
-
**Important.** The augmentation has to declare on the **class** (`StringValidator`), not the factory return type. The factory return widens with `& StandardSchemaV1<...>` (see [`@warlock.js/seal/bridge-standard-schema/SKILL.md`](@warlock.js/seal/bridge-standard-schema/SKILL.md)) — augmentations on the intersection don't propagate. Patch the class, augment the class; the factory return picks up the new methods through structural inference.
|
|
104
|
-
|
|
105
|
-
## Larger example — postal codes per country
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
109
|
-
|
|
110
|
-
const PATTERNS: Record<string, RegExp> = {
|
|
111
|
-
US: /^\d{5}(?:-\d{4})?$/,
|
|
112
|
-
DE: /^\d{5}$/,
|
|
113
|
-
UK: /^[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}$/i,
|
|
114
|
-
EG: /^\d{5}$/,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export const postalCodePlugin: SealPlugin = {
|
|
118
|
-
name: "postal-code",
|
|
119
|
-
|
|
120
|
-
install() {
|
|
121
|
-
Object.assign(StringValidator.prototype, {
|
|
122
|
-
postalCode(this: StringValidator, country: keyof typeof PATTERNS, errorMessage?: string) {
|
|
123
|
-
const pattern = PATTERNS[country];
|
|
124
|
-
|
|
125
|
-
if (!pattern) {
|
|
126
|
-
throw new Error(`postalCode: unknown country "${country}"`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return this.pattern(pattern, errorMessage ?? `Invalid ${country} postal code`);
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
// Module augmentation
|
|
138
|
-
declare module "@warlock.js/seal" {
|
|
139
|
-
interface StringValidator {
|
|
140
|
-
postalCode(country: "US" | "DE" | "UK" | "EG", errorMessage?: string): StringValidator;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Use site
|
|
145
|
-
const addressSchema = v.object({
|
|
146
|
-
country: v.literal("US", "DE", "UK", "EG"),
|
|
147
|
-
postal: v.string().postalCode("DE"),
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Patterns beyond `StringValidator`
|
|
152
|
-
|
|
153
|
-
The same approach works on any validator class. Pick the right prototype:
|
|
154
|
-
|
|
155
|
-
- `StringValidator` — string methods (`.slug`, `.postalCode`, `.licensePlate`).
|
|
156
|
-
- `NumberValidator` / `IntValidator` / `FloatValidator` — number methods.
|
|
157
|
-
- `DateValidator` — date methods (`.businessDayInCountry("US")`).
|
|
158
|
-
- `ArrayValidator` / `ObjectValidator` — structural methods (rarer).
|
|
159
|
-
- `BaseValidator` — universal methods (rare — usually a sign you want a separate `v.something()` factory instead).
|
|
160
|
-
|
|
161
|
-
For a method that creates a **new validator** (not chained from an existing one), expose it as a regular function alongside `v` rather than patching the factory. E.g. export `iban()` from your plugin module that returns a configured `v.string()`.
|
|
162
|
-
|
|
163
|
-
## Introspection — checking what's loaded
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
hasPlugin("slug"); // boolean
|
|
167
|
-
getInstalledPlugins(); // SealPlugin[]
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Use these in startup diagnostics or in tests that need to assert a plugin is registered before exercising a method that depends on it.
|
|
171
|
-
|
|
172
|
-
## Things NOT to do
|
|
173
|
-
|
|
174
|
-
- Don't `Object.assign(BaseValidator.prototype, ...)` for type-specific methods. The method would exist on every validator (`v.boolean().slug()` typechecks but breaks at runtime). Patch the narrowest class that owns the method.
|
|
175
|
-
- Don't forget the module augmentation. Without it, the new methods exist at runtime but TS rejects every call site.
|
|
176
|
-
- Don't make the plugin's `install` function depend on shared state mutable from elsewhere. Plugins should be idempotent — installing twice (or registering across hot-reloads) should not break anything.
|
|
177
|
-
- 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.
|
|
178
|
-
- 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.
|
|
1
|
+
---
|
|
2
|
+
name: extend-seal-with-plugins
|
|
3
|
+
description: 'Author a custom seal plugin to add validator methods (`.slug`, `.postalCode`, etc) — `SealPlugin` shape, `registerPlugin` lifecycle, TS prototype augmentation. Triggers: `SealPlugin`, `registerPlugin`, `unregisterPlugin`, `hasPlugin`, `getInstalledPlugins`, `StringValidator`, `NumberValidator`, `DateValidator`, `BaseValidator`, `install`, `uninstall`; "how do I add a custom validator method", "write a seal plugin", "extend v.string with .slug", "module augmentation for seal"; typical import `import { StringValidator, registerPlugin, type SealPlugin } from "@warlock.js/seal"`. Skip: built-in primitives — `@warlock.js/seal/pick-seal-primitive/SKILL.md`; bridge typing — `@warlock.js/seal/bridge-standard-schema/SKILL.md`; competing `zod` `.refine`/`.transform`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extend seal with plugins
|
|
7
|
+
|
|
8
|
+
Seal exposes a plugin system so you can add validator methods without forking the package. A plugin is an object with `name`, optional `version`/`description`, and an `install` function that runs once when the plugin is registered. The standard pattern is to `Object.assign(StringValidator.prototype, { ... })` to graft new methods onto a validator class.
|
|
9
|
+
|
|
10
|
+
## When to reach for a plugin
|
|
11
|
+
|
|
12
|
+
- The validator method you want **does not exist** in built-in seal — check [`@warlock.js/seal/pick-seal-primitive/SKILL.md`](@warlock.js/seal/pick-seal-primitive/SKILL.md) and its method references first.
|
|
13
|
+
- The validation is **stable and reusable** across modules — domain-specific formats (IBAN, postal codes, tax IDs, license plates, internal ID schemes).
|
|
14
|
+
- You want the **chainable syntax** — `v.string().slug()` reads better than `v.string().pattern(/.../).addMutator(s => slugify(s))` at every call site.
|
|
15
|
+
|
|
16
|
+
**Don't** reach for a plugin when a one-off `.pattern()` would do. The boilerplate (declare module, register on boot) is justified only when you'll call the new method many times.
|
|
17
|
+
|
|
18
|
+
## The plugin shape
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import type { SealPlugin } from "@warlock.js/seal";
|
|
22
|
+
|
|
23
|
+
type SealPlugin = {
|
|
24
|
+
name: string; // unique identifier — duplicates warn and skip install
|
|
25
|
+
version?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
install: (context: { name: string; version?: string }) => void | Promise<void>;
|
|
28
|
+
uninstall?: () => void | Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `install` function is where you add methods. Typically you patch a validator class prototype:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
36
|
+
|
|
37
|
+
export const slugPlugin: SealPlugin = {
|
|
38
|
+
name: "slug",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
description: "Adds .slug() — pattern-only slug validation",
|
|
41
|
+
|
|
42
|
+
install() {
|
|
43
|
+
Object.assign(StringValidator.prototype, {
|
|
44
|
+
slug(this: StringValidator, errorMessage?: string) {
|
|
45
|
+
return this.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, errorMessage);
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`uninstall` is optional. Provide it when your plugin needs to clean up — e.g. removing prototype methods for hot-reload scenarios. Most production plugins skip it; methods grafted at boot stay for the process lifetime.
|
|
53
|
+
|
|
54
|
+
## Registering plugins
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { registerPlugin, unregisterPlugin, hasPlugin, getInstalledPlugins } from "@warlock.js/seal";
|
|
58
|
+
|
|
59
|
+
await registerPlugin(slugPlugin);
|
|
60
|
+
// console.warn if "slug" is already installed; otherwise install() runs and the plugin is tracked.
|
|
61
|
+
|
|
62
|
+
hasPlugin("slug"); // → true
|
|
63
|
+
getInstalledPlugins(); // → [slugPlugin]
|
|
64
|
+
|
|
65
|
+
await unregisterPlugin("slug");
|
|
66
|
+
// runs slugPlugin.uninstall?.(); removes from registry.
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`registerPlugin` is async (the `install` function may be async). Await it during boot so the methods are available before the first request.
|
|
70
|
+
|
|
71
|
+
**Where to register:** the conventional place in a Warlock app is a side-effect file loaded by `warlock.config.ts` — e.g. `src/setup/seal-plugins.ts`:
|
|
72
|
+
|
|
73
|
+
```ts title="src/setup/seal-plugins.ts"
|
|
74
|
+
import { registerPlugin } from "@warlock.js/seal";
|
|
75
|
+
import { slugPlugin } from "./plugins/slug-plugin";
|
|
76
|
+
import { postalCodePlugin } from "./plugins/postal-code-plugin";
|
|
77
|
+
|
|
78
|
+
export async function setupSealPlugins() {
|
|
79
|
+
await registerPlugin(slugPlugin);
|
|
80
|
+
await registerPlugin(postalCodePlugin);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then call `setupSealPlugins()` in a bootstrap connector. Registering at top-level module scope works too (it's idempotent — duplicates warn and skip), but explicit setup is clearer.
|
|
85
|
+
|
|
86
|
+
## TypeScript — declare the new methods
|
|
87
|
+
|
|
88
|
+
`Object.assign` on a prototype is invisible to TypeScript. Declare the new methods with module augmentation so call sites compile:
|
|
89
|
+
|
|
90
|
+
```ts title="src/setup/seal-plugins.types.ts"
|
|
91
|
+
import "@warlock.js/seal";
|
|
92
|
+
|
|
93
|
+
declare module "@warlock.js/seal" {
|
|
94
|
+
interface StringValidator {
|
|
95
|
+
/** Pattern-only slug — `"hello-world"`, not `"Hello World"`. */
|
|
96
|
+
slug(errorMessage?: string): StringValidator;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
After this file is included in the project's `tsconfig.json` (via `include` or a side-effect import), `v.string().slug()` autocompletes and type-checks everywhere.
|
|
102
|
+
|
|
103
|
+
**Important.** The augmentation has to declare on the **class** (`StringValidator`), not the factory return type. The factory return widens with `& StandardSchemaV1<...>` (see [`@warlock.js/seal/bridge-standard-schema/SKILL.md`](@warlock.js/seal/bridge-standard-schema/SKILL.md)) — augmentations on the intersection don't propagate. Patch the class, augment the class; the factory return picks up the new methods through structural inference.
|
|
104
|
+
|
|
105
|
+
## Larger example — postal codes per country
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { StringValidator, type SealPlugin } from "@warlock.js/seal";
|
|
109
|
+
|
|
110
|
+
const PATTERNS: Record<string, RegExp> = {
|
|
111
|
+
US: /^\d{5}(?:-\d{4})?$/,
|
|
112
|
+
DE: /^\d{5}$/,
|
|
113
|
+
UK: /^[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2}$/i,
|
|
114
|
+
EG: /^\d{5}$/,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const postalCodePlugin: SealPlugin = {
|
|
118
|
+
name: "postal-code",
|
|
119
|
+
|
|
120
|
+
install() {
|
|
121
|
+
Object.assign(StringValidator.prototype, {
|
|
122
|
+
postalCode(this: StringValidator, country: keyof typeof PATTERNS, errorMessage?: string) {
|
|
123
|
+
const pattern = PATTERNS[country];
|
|
124
|
+
|
|
125
|
+
if (!pattern) {
|
|
126
|
+
throw new Error(`postalCode: unknown country "${country}"`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return this.pattern(pattern, errorMessage ?? `Invalid ${country} postal code`);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// Module augmentation
|
|
138
|
+
declare module "@warlock.js/seal" {
|
|
139
|
+
interface StringValidator {
|
|
140
|
+
postalCode(country: "US" | "DE" | "UK" | "EG", errorMessage?: string): StringValidator;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Use site
|
|
145
|
+
const addressSchema = v.object({
|
|
146
|
+
country: v.literal("US", "DE", "UK", "EG"),
|
|
147
|
+
postal: v.string().postalCode("DE"),
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Patterns beyond `StringValidator`
|
|
152
|
+
|
|
153
|
+
The same approach works on any validator class. Pick the right prototype:
|
|
154
|
+
|
|
155
|
+
- `StringValidator` — string methods (`.slug`, `.postalCode`, `.licensePlate`).
|
|
156
|
+
- `NumberValidator` / `IntValidator` / `FloatValidator` — number methods.
|
|
157
|
+
- `DateValidator` — date methods (`.businessDayInCountry("US")`).
|
|
158
|
+
- `ArrayValidator` / `ObjectValidator` — structural methods (rarer).
|
|
159
|
+
- `BaseValidator` — universal methods (rare — usually a sign you want a separate `v.something()` factory instead).
|
|
160
|
+
|
|
161
|
+
For a method that creates a **new validator** (not chained from an existing one), expose it as a regular function alongside `v` rather than patching the factory. E.g. export `iban()` from your plugin module that returns a configured `v.string()`.
|
|
162
|
+
|
|
163
|
+
## Introspection — checking what's loaded
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
hasPlugin("slug"); // boolean
|
|
167
|
+
getInstalledPlugins(); // SealPlugin[]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use these in startup diagnostics or in tests that need to assert a plugin is registered before exercising a method that depends on it.
|
|
171
|
+
|
|
172
|
+
## Things NOT to do
|
|
173
|
+
|
|
174
|
+
- Don't `Object.assign(BaseValidator.prototype, ...)` for type-specific methods. The method would exist on every validator (`v.boolean().slug()` typechecks but breaks at runtime). Patch the narrowest class that owns the method.
|
|
175
|
+
- Don't forget the module augmentation. Without it, the new methods exist at runtime but TS rejects every call site.
|
|
176
|
+
- Don't make the plugin's `install` function depend on shared state mutable from elsewhere. Plugins should be idempotent — installing twice (or registering across hot-reloads) should not break anything.
|
|
177
|
+
- 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.
|
|
178
|
+
- 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.
|
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: generate-json-schema
|
|
3
|
-
description: 'Generate JSON Schema via `schema.toJsonSchema(target)` — `draft-2020-12` / `draft-07` / `openapi-3.0` / `openai-strict`. Triggers: `toJsonSchema`, `JsonSchemaTarget`, `draft-2020-12`, `draft-07`, `openapi-3.0`, `openai-strict`, `response_format`, `additionalProperties`, `nullable`; "how do I generate JSON Schema from seal", "OpenAI structured outputs from schema", "OpenAPI 3.0 nullable", "json_schema strict mode"; typical import `import { v } from "@warlock.js/seal"`. Skip: foundations — `@warlock.js/seal/seal-basics/SKILL.md`; bridge typing — `@warlock.js/seal/bridge-standard-schema/SKILL.md`; competing libs `zod-to-json-schema`, `ajv`, `@anatine/zod-openapi`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# JSON Schema generation
|
|
7
|
-
|
|
8
|
-
Every seal validator exposes `toJsonSchema(target)`. The result is a plain object — pass it straight to OpenAI's `response_format`, an OpenAPI spec, a UI form builder, or anywhere else JSON Schema is the contract.
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
const userSchema = v.object({
|
|
12
|
-
email: v.string().email(),
|
|
13
|
-
age: v.int().min(13).optional(),
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
userSchema.toJsonSchema("draft-2020-12");
|
|
17
|
-
// {
|
|
18
|
-
// type: "object",
|
|
19
|
-
// properties: {
|
|
20
|
-
// email: { type: "string", format: "email" },
|
|
21
|
-
// age: { type: "integer", minimum: 13 },
|
|
22
|
-
// },
|
|
23
|
-
// required: ["email"],
|
|
24
|
-
// additionalProperties: false,
|
|
25
|
-
// }
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## The four targets
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
type JsonSchemaTarget =
|
|
32
|
-
| "draft-2020-12" // default — modern JSON Schema
|
|
33
|
-
| "draft-07" // older tooling, Swagger 2.0
|
|
34
|
-
| "openapi-3.0" // uses { nullable: true } instead of type unions
|
|
35
|
-
| "openai-strict" // OpenAI Structured Outputs strict mode
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
Pick by consumer:
|
|
39
|
-
|
|
40
|
-
| Consumer | Target |
|
|
41
|
-
| --- | --- |
|
|
42
|
-
| Modern tooling, no specific reason otherwise | `"draft-2020-12"` |
|
|
43
|
-
| Swagger 2.0 / older OpenAPI / older form builders | `"draft-07"` |
|
|
44
|
-
| OpenAPI 3.0 spec (uses `nullable: true`) | `"openapi-3.0"` |
|
|
45
|
-
| OpenAI `response_format: { type: "json_schema", strict: true }` | `"openai-strict"` |
|
|
46
|
-
|
|
47
|
-
## OpenAI structured outputs (`openai-strict`)
|
|
48
|
-
|
|
49
|
-
This target encodes the quirks of OpenAI's strict mode:
|
|
50
|
-
|
|
51
|
-
- **Every field listed in `required`** — strict mode forbids leaving fields out.
|
|
52
|
-
- **Optional fields encoded as `type: ["T", "null"]`** instead of being omitted from `required`.
|
|
53
|
-
- **`additionalProperties: false` on every object.**
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
const schema = v.object({
|
|
57
|
-
reply: v.string(),
|
|
58
|
-
citations: v.array(v.string()).optional(),
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
schema.toJsonSchema("openai-strict");
|
|
62
|
-
// {
|
|
63
|
-
// type: "object",
|
|
64
|
-
// properties: {
|
|
65
|
-
// reply: { type: "string" },
|
|
66
|
-
// citations: { type: ["array", "null"], items: { type: "string" } },
|
|
67
|
-
// },
|
|
68
|
-
// required: ["reply", "citations"], // every field listed
|
|
69
|
-
// additionalProperties: false,
|
|
70
|
-
// }
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Hand to OpenAI:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
import OpenAI from "openai";
|
|
77
|
-
|
|
78
|
-
const completion = await openai.chat.completions.create({
|
|
79
|
-
model: "gpt-4o",
|
|
80
|
-
messages: [...],
|
|
81
|
-
response_format: {
|
|
82
|
-
type: "json_schema",
|
|
83
|
-
json_schema: {
|
|
84
|
-
name: "user_reply",
|
|
85
|
-
strict: true,
|
|
86
|
-
schema: schema.toJsonSchema("openai-strict"),
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
In `@warlock.js/ai`, this happens automatically when you set `output: schema` on a supervisor / agent — the runtime picks `openai-strict` for OpenAI providers. You only call `toJsonSchema()` directly when integrating with a non-warlock OpenAI usage.
|
|
93
|
-
|
|
94
|
-
## OpenAPI 3.0 nullable
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
v.string().nullable().toJsonSchema("openapi-3.0");
|
|
98
|
-
// { type: "string", nullable: true }
|
|
99
|
-
|
|
100
|
-
v.string().nullable().toJsonSchema("draft-2020-12");
|
|
101
|
-
// { type: ["string", "null"] }
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
OpenAPI 3.0 uses the boolean `nullable` keyword instead of a type union. Use this target when generating a `paths.openapi.yaml` consumed by Swagger UI or codegen tools.
|
|
105
|
-
|
|
106
|
-
## What's representable
|
|
107
|
-
|
|
108
|
-
Cleanly mapped:
|
|
109
|
-
|
|
110
|
-
- `v.string()` — `{ type: "string" }` (with `format: email/url/uuid`, `pattern`, `minLength`, `maxLength`, `enum`)
|
|
111
|
-
- `v.int()` / `v.float()` — `{ type: "integer" | "number" }` (with `minimum`, `maximum`, `multipleOf`)
|
|
112
|
-
- `v.boolean()` — `{ type: "boolean" }`
|
|
113
|
-
- `v.date()` — `{ type: "string", format: "date-time" | "date" | "time" }` (format derived from transformer if applicable)
|
|
114
|
-
- `v.literal(values)` — `{ const: value }` (single) or `{ enum: [...] }` (multiple)
|
|
115
|
-
- `v.array(item)` — `{ type: "array", items: ... }` (with `minItems`, `maxItems` from length rules; `.unique()`/`.sorted()` are runtime-only and not emitted)
|
|
116
|
-
- `v.object({...})` — `{ type: "object", properties, required, additionalProperties }`
|
|
117
|
-
- `v.union([...])` — `{ oneOf: [...] }`
|
|
118
|
-
- `v.tuple([...])` — `{ type: "array", prefixItems: [...] }` (draft-2020-12) or `{ type: "array", items: [...] }` (draft-07)
|
|
119
|
-
- `v.nullable()` — type union or `nullable: true` per target
|
|
120
|
-
|
|
121
|
-
## What's silently dropped
|
|
122
|
-
|
|
123
|
-
Some seal constructs have no JSON Schema representation:
|
|
124
|
-
|
|
125
|
-
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the *shape*, not the inter-field invariants.
|
|
126
|
-
- **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`).
|
|
127
|
-
- **`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.)
|
|
128
|
-
- **`v.instanceof(Ctor)`** — produces `{}`. Class identity isn't expressible. For `File`, attach `{ type: "string", format: "binary" }` manually after generation if needed for OpenAPI.
|
|
129
|
-
- **`v.any()`** — produces `{}` deliberately (any value is valid).
|
|
130
|
-
|
|
131
|
-
Boolean rules `accepted` / `declined` and similar coercion-style rules are also dropped — JSON Schema doesn't have a notion of "yes/no/on/off" beyond `enum`.
|
|
132
|
-
|
|
133
|
-
## When the generated schema rejects valid data
|
|
134
|
-
|
|
135
|
-
If the schema validator itself accepts data but the *generated* JSON Schema rejects the same data downstream, the cause is usually one of:
|
|
136
|
-
|
|
137
|
-
- **Cross-field rule.** The generated schema doesn't enforce it, but a separate consumer might. (Or the runtime check fired at a different stage.)
|
|
138
|
-
- **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.
|
|
139
|
-
- **`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.
|
|
140
|
-
|
|
141
|
-
## Cost note
|
|
142
|
-
|
|
143
|
-
Generating JSON Schema is cheap (pure-function tree walk), so don't worry about caching the result for schemas that change at startup. For dynamic schemas built per-request, generate per-request — there's no shared mutable state.
|
|
1
|
+
---
|
|
2
|
+
name: generate-json-schema
|
|
3
|
+
description: 'Generate JSON Schema via `schema.toJsonSchema(target)` — `draft-2020-12` / `draft-07` / `openapi-3.0` / `openai-strict`. Triggers: `toJsonSchema`, `JsonSchemaTarget`, `draft-2020-12`, `draft-07`, `openapi-3.0`, `openai-strict`, `response_format`, `additionalProperties`, `nullable`; "how do I generate JSON Schema from seal", "OpenAI structured outputs from schema", "OpenAPI 3.0 nullable", "json_schema strict mode"; typical import `import { v } from "@warlock.js/seal"`. Skip: foundations — `@warlock.js/seal/seal-basics/SKILL.md`; bridge typing — `@warlock.js/seal/bridge-standard-schema/SKILL.md`; competing libs `zod-to-json-schema`, `ajv`, `@anatine/zod-openapi`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# JSON Schema generation
|
|
7
|
+
|
|
8
|
+
Every seal validator exposes `toJsonSchema(target)`. The result is a plain object — pass it straight to OpenAI's `response_format`, an OpenAPI spec, a UI form builder, or anywhere else JSON Schema is the contract.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const userSchema = v.object({
|
|
12
|
+
email: v.string().email(),
|
|
13
|
+
age: v.int().min(13).optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
userSchema.toJsonSchema("draft-2020-12");
|
|
17
|
+
// {
|
|
18
|
+
// type: "object",
|
|
19
|
+
// properties: {
|
|
20
|
+
// email: { type: "string", format: "email" },
|
|
21
|
+
// age: { type: "integer", minimum: 13 },
|
|
22
|
+
// },
|
|
23
|
+
// required: ["email"],
|
|
24
|
+
// additionalProperties: false,
|
|
25
|
+
// }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## The four targets
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
type JsonSchemaTarget =
|
|
32
|
+
| "draft-2020-12" // default — modern JSON Schema
|
|
33
|
+
| "draft-07" // older tooling, Swagger 2.0
|
|
34
|
+
| "openapi-3.0" // uses { nullable: true } instead of type unions
|
|
35
|
+
| "openai-strict" // OpenAI Structured Outputs strict mode
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Pick by consumer:
|
|
39
|
+
|
|
40
|
+
| Consumer | Target |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Modern tooling, no specific reason otherwise | `"draft-2020-12"` |
|
|
43
|
+
| Swagger 2.0 / older OpenAPI / older form builders | `"draft-07"` |
|
|
44
|
+
| OpenAPI 3.0 spec (uses `nullable: true`) | `"openapi-3.0"` |
|
|
45
|
+
| OpenAI `response_format: { type: "json_schema", strict: true }` | `"openai-strict"` |
|
|
46
|
+
|
|
47
|
+
## OpenAI structured outputs (`openai-strict`)
|
|
48
|
+
|
|
49
|
+
This target encodes the quirks of OpenAI's strict mode:
|
|
50
|
+
|
|
51
|
+
- **Every field listed in `required`** — strict mode forbids leaving fields out.
|
|
52
|
+
- **Optional fields encoded as `type: ["T", "null"]`** instead of being omitted from `required`.
|
|
53
|
+
- **`additionalProperties: false` on every object.**
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const schema = v.object({
|
|
57
|
+
reply: v.string(),
|
|
58
|
+
citations: v.array(v.string()).optional(),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
schema.toJsonSchema("openai-strict");
|
|
62
|
+
// {
|
|
63
|
+
// type: "object",
|
|
64
|
+
// properties: {
|
|
65
|
+
// reply: { type: "string" },
|
|
66
|
+
// citations: { type: ["array", "null"], items: { type: "string" } },
|
|
67
|
+
// },
|
|
68
|
+
// required: ["reply", "citations"], // every field listed
|
|
69
|
+
// additionalProperties: false,
|
|
70
|
+
// }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Hand to OpenAI:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import OpenAI from "openai";
|
|
77
|
+
|
|
78
|
+
const completion = await openai.chat.completions.create({
|
|
79
|
+
model: "gpt-4o",
|
|
80
|
+
messages: [...],
|
|
81
|
+
response_format: {
|
|
82
|
+
type: "json_schema",
|
|
83
|
+
json_schema: {
|
|
84
|
+
name: "user_reply",
|
|
85
|
+
strict: true,
|
|
86
|
+
schema: schema.toJsonSchema("openai-strict"),
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
In `@warlock.js/ai`, this happens automatically when you set `output: schema` on a supervisor / agent — the runtime picks `openai-strict` for OpenAI providers. You only call `toJsonSchema()` directly when integrating with a non-warlock OpenAI usage.
|
|
93
|
+
|
|
94
|
+
## OpenAPI 3.0 nullable
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
v.string().nullable().toJsonSchema("openapi-3.0");
|
|
98
|
+
// { type: "string", nullable: true }
|
|
99
|
+
|
|
100
|
+
v.string().nullable().toJsonSchema("draft-2020-12");
|
|
101
|
+
// { type: ["string", "null"] }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
OpenAPI 3.0 uses the boolean `nullable` keyword instead of a type union. Use this target when generating a `paths.openapi.yaml` consumed by Swagger UI or codegen tools.
|
|
105
|
+
|
|
106
|
+
## What's representable
|
|
107
|
+
|
|
108
|
+
Cleanly mapped:
|
|
109
|
+
|
|
110
|
+
- `v.string()` — `{ type: "string" }` (with `format: email/url/uuid`, `pattern`, `minLength`, `maxLength`, `enum`)
|
|
111
|
+
- `v.int()` / `v.float()` — `{ type: "integer" | "number" }` (with `minimum`, `maximum`, `multipleOf`)
|
|
112
|
+
- `v.boolean()` — `{ type: "boolean" }`
|
|
113
|
+
- `v.date()` — `{ type: "string", format: "date-time" | "date" | "time" }` (format derived from transformer if applicable)
|
|
114
|
+
- `v.literal(values)` — `{ const: value }` (single) or `{ enum: [...] }` (multiple)
|
|
115
|
+
- `v.array(item)` — `{ type: "array", items: ... }` (with `minItems`, `maxItems` from length rules; `.unique()`/`.sorted()` are runtime-only and not emitted)
|
|
116
|
+
- `v.object({...})` — `{ type: "object", properties, required, additionalProperties }`
|
|
117
|
+
- `v.union([...])` — `{ oneOf: [...] }`
|
|
118
|
+
- `v.tuple([...])` — `{ type: "array", prefixItems: [...] }` (draft-2020-12) or `{ type: "array", items: [...] }` (draft-07)
|
|
119
|
+
- `v.nullable()` — type union or `nullable: true` per target
|
|
120
|
+
|
|
121
|
+
## What's silently dropped
|
|
122
|
+
|
|
123
|
+
Some seal constructs have no JSON Schema representation:
|
|
124
|
+
|
|
125
|
+
- **Cross-field rules** (`sameAs`, `requiredIf`, `requiredWith`, etc.) — runtime-only. The generated schema describes the *shape*, not the inter-field invariants.
|
|
126
|
+
- **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`).
|
|
127
|
+
- **`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.)
|
|
128
|
+
- **`v.instanceof(Ctor)`** — produces `{}`. Class identity isn't expressible. For `File`, attach `{ type: "string", format: "binary" }` manually after generation if needed for OpenAPI.
|
|
129
|
+
- **`v.any()`** — produces `{}` deliberately (any value is valid).
|
|
130
|
+
|
|
131
|
+
Boolean rules `accepted` / `declined` and similar coercion-style rules are also dropped — JSON Schema doesn't have a notion of "yes/no/on/off" beyond `enum`.
|
|
132
|
+
|
|
133
|
+
## When the generated schema rejects valid data
|
|
134
|
+
|
|
135
|
+
If the schema validator itself accepts data but the *generated* JSON Schema rejects the same data downstream, the cause is usually one of:
|
|
136
|
+
|
|
137
|
+
- **Cross-field rule.** The generated schema doesn't enforce it, but a separate consumer might. (Or the runtime check fired at a different stage.)
|
|
138
|
+
- **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.
|
|
139
|
+
- **`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.
|
|
140
|
+
|
|
141
|
+
## Cost note
|
|
142
|
+
|
|
143
|
+
Generating JSON Schema is cheap (pure-function tree walk), so don't worry about caching the result for schemas that change at startup. For dynamic schemas built per-request, generate per-request — there's no shared mutable state.
|