@jarenjs/validate 0.8.4 → 0.34.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/ARCHITECTURE.md +1131 -0
- package/LICENSE +21 -0
- package/README.md +796 -2
- package/dist/types/array.d.ts +2 -0
- package/dist/types/bigint.d.ts +1 -0
- package/dist/types/combine.d.ts +1 -0
- package/dist/types/condition.d.ts +1 -0
- package/dist/types/content.d.ts +3 -0
- package/dist/types/data.d.ts +7 -0
- package/dist/types/dollar-data.d.ts +11 -0
- package/dist/types/dynamic-ref.d.ts +44 -0
- package/dist/types/enum.d.ts +1 -0
- package/dist/types/format.d.ts +21 -0
- package/dist/types/index.d.ts +972 -0
- package/dist/types/messages.d.ts +142 -0
- package/dist/types/normalize.d.ts +107 -0
- package/dist/types/number.d.ts +1 -0
- package/dist/types/object.d.ts +3 -0
- package/dist/types/query-keyword.d.ts +19 -0
- package/dist/types/query.d.ts +29 -0
- package/dist/types/schema.d.ts +1 -0
- package/dist/types/string.d.ts +1 -0
- package/dist/types/tools.d.ts +109 -0
- package/dist/types/traverse.d.ts +32 -0
- package/dist/types/unevaluated.d.ts +12 -0
- package/docs/ERROR-MESSAGES.md +251 -0
- package/package.json +37 -7
- package/src/array.js +610 -0
- package/src/bigint.js +108 -0
- package/src/combine.js +276 -0
- package/src/condition.js +129 -0
- package/src/content.js +83 -0
- package/src/data.js +101 -0
- package/src/dollar-data.js +212 -0
- package/src/dynamic-ref.js +121 -0
- package/src/enum.js +147 -0
- package/src/format.js +108 -0
- package/src/index.js +1896 -0
- package/src/messages.js +497 -0
- package/src/normalize.js +585 -0
- package/src/number.js +169 -0
- package/src/object.js +848 -0
- package/src/query-keyword.js +99 -0
- package/src/query.js +85 -0
- package/src/schema.js +690 -0
- package/src/string.js +164 -0
- package/src/tools.js +397 -0
- package/src/traverse.js +442 -0
- package/src/unevaluated.js +173 -0
- package/dist/index.js +0 -1998
- package/dist/index.js.map +0 -7
- package/dist/index.min.js +0 -2
- package/dist/index.min.js.map +0 -7
package/README.md
CHANGED
|
@@ -1,3 +1,797 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @jarenjs/validate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The JSON Schema validating compiler at the heart of [Jaren](https://github.com/jklarenbeek/jarenjs). It compiles JSON Schemas into optimized validation functions and fully supports `draft-06`, `draft-07`, `draft 2019-09` and `draft 2020-12` — passing 100% of the official [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) for draft-07, 2019-09 and 2020-12.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
9
|
+
|
|
10
|
+
const jaren = new JarenValidator();
|
|
11
|
+
|
|
12
|
+
const validate = jaren.compile({
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
name: { type: 'string' },
|
|
16
|
+
age: { type: 'integer', minimum: 0 }
|
|
17
|
+
},
|
|
18
|
+
required: ['name']
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
validate({ name: 'John', age: 30 }); // true
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Key entry points:
|
|
25
|
+
|
|
26
|
+
- `new JarenValidator(options)` — create a validator instance
|
|
27
|
+
- `.addSchema(schema, key)` — register schemas for `$ref` resolution
|
|
28
|
+
- `.addMetaSchema(schemas, key)` — register (custom) meta-schemas, honoring `$vocabulary`
|
|
29
|
+
- `.addFormats(formats)` — register format validators (see [`@jarenjs/formats`](../formats))
|
|
30
|
+
- `.compile(schema)` — compile a schema into a validation function
|
|
31
|
+
|
|
32
|
+
Highlights:
|
|
33
|
+
|
|
34
|
+
- Annotation-based `unevaluatedProperties`/`unevaluatedItems`
|
|
35
|
+
- Spec-compliant dynamic scope for `$dynamicRef`/`$dynamicAnchor` and `$recursiveRef`/`$recursiveAnchor`
|
|
36
|
+
- Per-document draft handling for cross-draft references
|
|
37
|
+
- `$vocabulary`-aware keyword selection and per-draft `format`/content assertion defaults
|
|
38
|
+
- Instance-data references via the `data` keyword (json-everything data-ref) and Ajv-style `$data`
|
|
39
|
+
- Cross-field assertions via the `$query` extension keyword (a [Jaren JSON Query](../json/docs/QUERY-FORMAT.md) inside the schema)
|
|
40
|
+
|
|
41
|
+
## 🔑 JSON Schema validation keywords
|
|
42
|
+
|
|
43
|
+
Jaren supports the full set of JSON Schema validation keywords. Here's a quick overview:
|
|
44
|
+
|
|
45
|
+
- JSON data type: `type`, `nullable`, `required`
|
|
46
|
+
- Numbers: `maximum`, `minimum`, `multipleOf`
|
|
47
|
+
- Strings: `maxLength`, `minLength`, `pattern`
|
|
48
|
+
- Content: `contentEncoding`, `contentMediaType`
|
|
49
|
+
- Arrays: `maxItems`, `minItems`, `uniqueItems`, `items`, `prefixItems`, `contains`, `unevaluatedItems`
|
|
50
|
+
- Objects: `maxProperties`, `minProperties`, `required`, `properties`, `patternProperties`, `unevaluatedProperties`
|
|
51
|
+
- All types: `enum`, `const`
|
|
52
|
+
- Compound: `not`, `oneOf`, `anyOf`, `allOf`, `if/then/else`
|
|
53
|
+
- Meta: `$schema`, `$id`, `$ref`, `$anchor`, `$dynamicRef`/`$dynamicAnchor`, `$recursiveRef`/`$recursiveAnchor`, `$vocabulary`
|
|
54
|
+
- **Non-standard**: `data` (json-everything's [data-ref](https://docs.json-everything.net/schema/examples/data-ref/) proposal), Ajv-style `$data` references, and the [`$query` keyword](#query--cross-field-assertions) below
|
|
55
|
+
|
|
56
|
+
<details>
|
|
57
|
+
<summary>🔥 For a complete list of supported keywords and their implementation status, click here</summary>
|
|
58
|
+
|
|
59
|
+
### 🔑 JSON data type
|
|
60
|
+
|
|
61
|
+
- type
|
|
62
|
+
- nullable | _(OpenAPI)_
|
|
63
|
+
- required | _as boolean (OpenAPI)_
|
|
64
|
+
|
|
65
|
+
### 🔑 Keywords for numbers
|
|
66
|
+
|
|
67
|
+
- maximum / minimum<br />or exclusiveMaximum / exclusiveMinimum
|
|
68
|
+
- multipleOf
|
|
69
|
+
|
|
70
|
+
BigInt instance values are supported too: `maximum`/`minimum`/`exclusiveMaximum`/`exclusiveMinimum` and `multipleOf` compile dedicated BigInt comparators when the data is a `bigint`.
|
|
71
|
+
|
|
72
|
+
### 🔑 Keywords for strings
|
|
73
|
+
|
|
74
|
+
- maxLength / minLength
|
|
75
|
+
- pattern
|
|
76
|
+
|
|
77
|
+
### 🔑 Keywords for content
|
|
78
|
+
|
|
79
|
+
- contentEncoding | asserts in `draft7`, annotation-only from `draft2019` on (spec default), controlled by the `contentValidation` option
|
|
80
|
+
- contentMediaType | same assertion defaults; `application/json` content is parse-checked
|
|
81
|
+
- ❌ contentSchema | annotation only (never asserted)
|
|
82
|
+
|
|
83
|
+
### 🔑 Keywords for format
|
|
84
|
+
|
|
85
|
+
- format
|
|
86
|
+
- formatMinimum / formatMaximum<br />or formatExclusiveMinimum / formatExclusiveMaximum
|
|
87
|
+
|
|
88
|
+
### 🔑 Keywords for array
|
|
89
|
+
|
|
90
|
+
- maxItems / minItems
|
|
91
|
+
- uniqueItems
|
|
92
|
+
- items
|
|
93
|
+
- items | as schema or tuple _deprecated in `draft2020`_
|
|
94
|
+
- items | as schema only _new `draft2020`_
|
|
95
|
+
- prefixItems | as tuple _new `draft2020`_
|
|
96
|
+
- additionalItems | as schema _deprecated in `draft2020`_
|
|
97
|
+
- contains
|
|
98
|
+
- maxContains / minContains | _new `draft2019`_
|
|
99
|
+
- unevaluatedItems | _new `draft2019`_
|
|
100
|
+
|
|
101
|
+
### 🔑 Keywords for object
|
|
102
|
+
|
|
103
|
+
- maxProperties / minProperties
|
|
104
|
+
- required | _as array!_
|
|
105
|
+
- properties
|
|
106
|
+
- patternProperties
|
|
107
|
+
- additionalProperties
|
|
108
|
+
- dependencies | _deprecated in `draft2019`_
|
|
109
|
+
- dependentRequired | _new `draft2019`_
|
|
110
|
+
- dependentSchemas | _new `draft2019`_
|
|
111
|
+
- propertyNames
|
|
112
|
+
- unevaluatedProperties | _new `draft2019`_
|
|
113
|
+
- ❌ [propertyDependencies](https://github.com/json-schema-org/json-schema-spec/blob/main/proposals/propertyDependencies.md)
|
|
114
|
+
|
|
115
|
+
### 🔑 Keywords for all types
|
|
116
|
+
|
|
117
|
+
- enum
|
|
118
|
+
- const
|
|
119
|
+
|
|
120
|
+
### 🔑 Compound keywords
|
|
121
|
+
|
|
122
|
+
- not
|
|
123
|
+
- oneOf
|
|
124
|
+
- anyOf
|
|
125
|
+
- allOf
|
|
126
|
+
- if / then / else
|
|
127
|
+
|
|
128
|
+
See also:
|
|
129
|
+
- [Schema Composition](https://json-schema.org/understanding-json-schema/reference/combining)
|
|
130
|
+
- [Applying Subschemas Conditionally](https://json-schema.org/understanding-json-schema/reference/conditionals)
|
|
131
|
+
|
|
132
|
+
### 🔑 Meta keywords
|
|
133
|
+
|
|
134
|
+
- $schema | used for draft detection, vocabulary selection and cross-draft references
|
|
135
|
+
- $id
|
|
136
|
+
- $ref
|
|
137
|
+
- $anchor
|
|
138
|
+
- $recursiveRef | _new `draft2019` & deprecated in `draft2020`_
|
|
139
|
+
- $recursiveAnchor | _new `draft2019` & deprecated in `draft2020`_
|
|
140
|
+
- $dynamicRef | _new `draft2020`_
|
|
141
|
+
- $dynamicAnchor | _new `draft2020`_
|
|
142
|
+
- $data | _(Ajv specific)_
|
|
143
|
+
- [$vocabulary](https://github.com/json-schema-org/json-schema-spec/blob/main/proposals/vocabularies.md) | _new `draft2019`_ - a custom metaschema that omits the validation vocabulary turns keywords like `type` and `minimum` into annotations; a metaschema that declares the `format-assertion` vocabulary turns format assertion on
|
|
144
|
+
|
|
145
|
+
### 🔑 Non-standard keywords
|
|
146
|
+
|
|
147
|
+
- `data` | json-everything's [data-ref](https://docs.json-everything.net/schema/examples/data-ref/) proposal
|
|
148
|
+
|
|
149
|
+
The `data` keyword allows you to reference values from the instance being validated, enabling dynamic constraints based on other parts of the data.
|
|
150
|
+
|
|
151
|
+
**Example - Requiring B >= A:**
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {
|
|
156
|
+
"A": { "type": "number" },
|
|
157
|
+
"B": {
|
|
158
|
+
"type": "number",
|
|
159
|
+
"data": {
|
|
160
|
+
"minimum": "/A"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
- Passes: `{ "A": 5, "B": 10 }` (10 >= 5)
|
|
167
|
+
- Fails: `{ "A": 15, "B": 10 }` (10 < 15)
|
|
168
|
+
|
|
169
|
+
**Example - Enum from instance array:**
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"type": "object",
|
|
173
|
+
"properties": {
|
|
174
|
+
"color": {
|
|
175
|
+
"data": {
|
|
176
|
+
"enum": "/validColors"
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"validColors": {
|
|
180
|
+
"type": "array",
|
|
181
|
+
"items": { "type": "string" }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Supported keywords within `data`:**
|
|
188
|
+
- Number constraints: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`
|
|
189
|
+
- String constraints: `minLength`, `maxLength`, `pattern`, `format`
|
|
190
|
+
- Array constraints: `minItems`, `maxItems`
|
|
191
|
+
- Object constraints: `minProperties`, `maxProperties`
|
|
192
|
+
- Value constraints: `enum`, `const`
|
|
193
|
+
|
|
194
|
+
Both absolute JSON Pointers (e.g., `/A`, `/limits/min`) and relative JSON Pointers (e.g., `0/parent`, `1/sibling`) are supported.
|
|
195
|
+
|
|
196
|
+
- `$data` | Ajv-style instance references (`{ "minimum": { "$data": "1/limit" } }`) — supports every keyword the `data` list above supports, plus `uniqueItems` and `required`. Takes the same three reference forms: empty for the whole instance, an absolute JSON Pointer (`/limit`), or a Relative JSON Pointer (`1/limit`). Refs compile once at schema compile time through the compiled pointer engine of `@jarenjs/json`, and a reference that cannot be compiled **fails the compile** rather than quietly resolving to nothing — a constraint that disables itself is worse than a rejected schema.
|
|
197
|
+
|
|
198
|
+
- `$query` | Jaren's cross-field assertion keyword — see [below](#query--cross-field-assertions)
|
|
199
|
+
|
|
200
|
+
### 🔑 Miscellaneous keywords
|
|
201
|
+
|
|
202
|
+
- ❌ strict
|
|
203
|
+
- ❌ strictFormat
|
|
204
|
+
- ❌ strictTuple
|
|
205
|
+
- errorMessage | author-supplied messages that override text (never structure) — see [Error messages & i18n](#error-messages--i18n)
|
|
206
|
+
- definitions | used by initial schema traversal _deprecated in `draft2019`_
|
|
207
|
+
- $defs | used by initial schema traversal _new `draft2019`_
|
|
208
|
+
- components | _(OpenAPI)_
|
|
209
|
+
|
|
210
|
+
</details>
|
|
211
|
+
|
|
212
|
+
## 🛠️ Notable capabilities
|
|
213
|
+
|
|
214
|
+
### 👉 Modelling Inheritance with JSON Schema
|
|
215
|
+
|
|
216
|
+
Jaren fully supports `unevaluatedProperties`, so the inheritance patterns from the [Modelling Inheritance](https://json-schema.org/blog/posts/modelling-inheritance) blog post work out of the box. Annotations flow from `properties`, `patternProperties`, `additionalProperties` and every in-place applicator (`allOf`/`anyOf`/`oneOf`/`if-then-else`/`$ref`/`dependentSchemas`), with annotations from failed branches correctly discarded.
|
|
217
|
+
|
|
218
|
+
See also:
|
|
219
|
+
- [json-schema-core](https://json-schema.org/draft/2020-12/json-schema-core#name-unevaluatedproperties)
|
|
220
|
+
- [Combining unevaluatedProperties and ref: # #375](https://github.com/orgs/json-schema-org/discussions/375)
|
|
221
|
+
|
|
222
|
+
### 👉 Express array constraints more cleanly
|
|
223
|
+
|
|
224
|
+
Jaren fully supports `unevaluatedItems`, covering the array patterns from the 2020-12 [release notes](https://json-schema.org/draft/2020-12/release-notes#contains-and-unevaluateditems): items evaluated by `items`, `prefixItems`, `additionalItems` and (in 2020-12) `contains` are tracked, and everything left over is validated by the `unevaluatedItems` schema.
|
|
225
|
+
|
|
226
|
+
### 👉 Using Dynamic References to Support Generic Types
|
|
227
|
+
|
|
228
|
+
Jaren fully supports `$dynamicRef`/`$dynamicAnchor` (2020-12) and `$recursiveRef`/`$recursiveAnchor` (2019-09), including the generic-type patterns from the [dynamicRef and generics](https://json-schema.org/blog/posts/dynamicref-and-generics) blog post. Resolution follows the specification's dynamic-scope rules: entering a schema resource brings all of its dynamic anchors into scope, and a `$dynamicRef` resolves to the anchor in the outermost resource of the dynamic scope.
|
|
229
|
+
|
|
230
|
+
See also:
|
|
231
|
+
- [Understanding lexical dynamic scopes](https://json-schema.org/blog/posts/understanding-lexical-dynamic-scopes)
|
|
232
|
+
- [$dynamicRef and $dynamicAnchor](https://json-schema.org/draft/2020-12/release-notes#dollardynamicref-and-dollardynamicanchor)
|
|
233
|
+
|
|
234
|
+
### 👉 Runtime schema manipulation of constraints
|
|
235
|
+
|
|
236
|
+
Jaren supports the `data-ref` proposal from json-everything through the `data` keyword, plus Ajv-style `$data` references. Both allow a schema constraint to take its value from the instance being validated:
|
|
237
|
+
|
|
238
|
+
- Absolute JSON Pointers (e.g., `/A`, `/limits/min`)
|
|
239
|
+
- Relative JSON Pointers (e.g., `0/parent`, `1/sibling`)
|
|
240
|
+
- All common constraint keywords: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `minLength`, `maxLength`, `pattern`, `format`, `enum`, `const`, `minItems`, `maxItems`, `minProperties`, `maxProperties`
|
|
241
|
+
|
|
242
|
+
The refs compile once at schema compile time through the compiled pointer engine of [`@jarenjs/json`](../json) and resolve allocation-free per validation.
|
|
243
|
+
|
|
244
|
+
See also:
|
|
245
|
+
- [$data](https://github.com/json-schema-org/json-schema-spec/issues/51)
|
|
246
|
+
- [Ajv $data spec](https://github.com/ajv-validator/ajv/tree/master/spec/extras/%24data)
|
|
247
|
+
- [data-ref](https://docs.json-everything.net/schema/examples/data-ref/)
|
|
248
|
+
|
|
249
|
+
### 👉 Vocabularies and cross-draft references
|
|
250
|
+
|
|
251
|
+
A schema's `$schema` declaration is honored per document: referenced documents that declare a different draft are processed with that draft's keyword set (a draft-07 document ignores `dependentRequired`; a 2019-09 document ignores `prefixItems`). Custom metaschemas with `$vocabulary` are respected — omitting the validation vocabulary turns validation keywords into annotations, and declaring `format-assertion` turns format assertion on.
|
|
252
|
+
|
|
253
|
+
## `$query` — cross-field assertions
|
|
254
|
+
|
|
255
|
+
`$query` is a **Jaren extension keyword**: its value is a
|
|
256
|
+
[Jaren JSON Query](../json/docs/QUERY-FORMAT.md) document, compiled once at
|
|
257
|
+
schema compile time and evaluated per validation against the current instance
|
|
258
|
+
location. The instance is valid when the query result's
|
|
259
|
+
[effective boolean value](../json/docs/QUERY-FORMAT.md#22-effective-boolean-value-ebv)
|
|
260
|
+
is true. This gives JSON Schema the class of constraint it is notoriously bad
|
|
261
|
+
at — cross-field arithmetic, ordering, aggregate consistency, quantification —
|
|
262
|
+
through the query engine that already sits underneath the stack. Other
|
|
263
|
+
validators treat `$query` as an unknown-keyword annotation, so schemas using
|
|
264
|
+
it stay portable; the constraint simply only asserts here.
|
|
265
|
+
|
|
266
|
+
An invoice whose `total` must equal the sum of its line amounts:
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
const validate = jaren.compile({
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
lines: { type: 'array', items: { type: 'object' } },
|
|
273
|
+
total: { type: 'number' }
|
|
274
|
+
},
|
|
275
|
+
$query: { $eq: ['$.total', { $sum: '$.lines[*].amount' }] }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
validate({ lines: [{ amount: 12.5 }, { amount: 7.5 }], total: 20 }); // true
|
|
279
|
+
validate({ lines: [{ amount: 12.5 }, { amount: 7.5 }], total: 21 }); // false
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Date ordering (`$le` compares strings by code points, QUERY-FORMAT §8.4 —
|
|
283
|
+
exactly right for ISO dates):
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
jaren.compile({ $query: { $le: ['$.start', '$.end'] } });
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Quantification over items:
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
jaren.compile({
|
|
293
|
+
$query: { $every: { l: '$.lines[*]' }, $satisfies: { $gt: ['$l.qty', 0] } }
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Two external parameters are bound on every evaluation: `root` — the instance
|
|
298
|
+
root — and `path` — the current instance location as a JSON pointer string.
|
|
299
|
+
(The JSLT template layer reserves the same two names with one twist: its
|
|
300
|
+
matching language is JSONPath, so its `path` is an RFC 9535 *normalized
|
|
301
|
+
path*, not a pointer — see [JSLT-FORMAT §8.2](../json/docs/JSLT-FORMAT.md).)
|
|
302
|
+
So a subschema can reach across the document:
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
jaren.compile({
|
|
306
|
+
type: 'object',
|
|
307
|
+
properties: {
|
|
308
|
+
lines: {
|
|
309
|
+
type: 'array',
|
|
310
|
+
items: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
// every line's currency must match the document-level currency
|
|
313
|
+
$query: { $eq: ['$.currency', '$root.currency'] }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Semantics and composition:
|
|
321
|
+
|
|
322
|
+
- `$query` works at **any subschema level** and composes like every other
|
|
323
|
+
keyword: under `properties`/`items` the query's `$` is that location's
|
|
324
|
+
value, `$path` its pointer (`/lines/0`, ...), `$root` the whole document.
|
|
325
|
+
- A bare JSONPath string is the degenerate query: `{ "$query": "$.approved" }`
|
|
326
|
+
asserts the **EBV** of that member, not its mere existence: existence would
|
|
327
|
+
let `$.approved` pass on a literal `false`, exactly the case the constraint
|
|
328
|
+
means to reject (missing → empty sequence → false either way).
|
|
329
|
+
- `$query` is a **validation keyword**, so under 2019-09 and 2020-12 it
|
|
330
|
+
asserts as a sibling of `$ref` (both apply together). Under draft-07 the
|
|
331
|
+
`$ref`-overrides-siblings rule stands, so a `$query` written beside a `$ref`
|
|
332
|
+
is ignored on that node.
|
|
333
|
+
- Query **runtime** errors (`JQ2xxx` — e.g. arithmetic on a non-number, the
|
|
334
|
+
EBV of a multi-item result) are validation **failures**, never throws; in
|
|
335
|
+
`collectErrors` mode the error params carry the `code` and the query
|
|
336
|
+
`docPath`. Malformed query documents and free externals other than
|
|
337
|
+
`root`/`path` fail fast at `compile()`.
|
|
338
|
+
- Schema literals inside the query (`$valid`/`$assert`/`$as`,
|
|
339
|
+
QUERY-FORMAT §8.11) compile against the **same validator instance**, so
|
|
340
|
+
their `$ref`s resolve to your `addSchema` registrations. The bridge
|
|
341
|
+
(`createTypeTestCompiler`) accepts nothing (a fresh default instance), a
|
|
342
|
+
`JarenValidator` instance, or a zero-arg factory, and probes the compiled
|
|
343
|
+
validator's return shape **once per schema literal** — so even a
|
|
344
|
+
`collectErrors` instance is unwrapped into a boolean predicate.
|
|
345
|
+
|
|
346
|
+
## Compatibility settings
|
|
347
|
+
|
|
348
|
+
Five options decide answers that differ between validators, between JSON
|
|
349
|
+
Schema drafts, or between Jaren and the library you are migrating from. Each
|
|
350
|
+
one is a deliberate default, and each one is worth setting explicitly in a
|
|
351
|
+
shared factory rather than inheriting.
|
|
352
|
+
|
|
353
|
+
| Option | Default | What it decides |
|
|
354
|
+
|---|---|---|
|
|
355
|
+
| `collectErrors` | `false` | Whether the compiled validator returns a boolean or `{ valid, errors }` |
|
|
356
|
+
| `skipErrors` | `!collectErrors` | Whether validation stops at the first failure |
|
|
357
|
+
| `useGrapheme` | **`true`** | Whether `minLength`/`maxLength` count grapheme clusters or UTF-16 code units |
|
|
358
|
+
| `formatAssertion` | auto by draft | Whether `format` asserts or only annotates |
|
|
359
|
+
| `contentValidation` | auto by draft | Whether `contentEncoding`/`contentMediaType` assert |
|
|
360
|
+
|
|
361
|
+
### The return shape is `collectErrors`
|
|
362
|
+
|
|
363
|
+
`collectErrors` is the *only* switch between the two return shapes, and it is
|
|
364
|
+
off by default:
|
|
365
|
+
|
|
366
|
+
```javascript
|
|
367
|
+
new JarenValidator().compile(schema)(data);
|
|
368
|
+
// => true | false
|
|
369
|
+
|
|
370
|
+
new JarenValidator({ collectErrors: true }).compile(schema)(data);
|
|
371
|
+
// => { valid: false, errors: [ /* ValidationError */ ] }
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Setting `collectErrors: true` implies `skipErrors: false` (collecting errors
|
|
375
|
+
means recording them), so you do not need to set both. Set `skipErrors`
|
|
376
|
+
yourself only to keep first-failure short-circuiting while still collecting.
|
|
377
|
+
|
|
378
|
+
There is **no `validator.errors` property**. Errors arrive in the returned
|
|
379
|
+
object and nowhere else, which is what makes a compiled validator reentrant
|
|
380
|
+
and safe to share across concurrent requests.
|
|
381
|
+
|
|
382
|
+
Each `ValidationError` carries six fields:
|
|
383
|
+
|
|
384
|
+
```javascript
|
|
385
|
+
{
|
|
386
|
+
keyword: 'format', // the JSON Schema keyword that failed
|
|
387
|
+
instancePath: '/email', // RFC 6901 JSON Pointer into the DATA
|
|
388
|
+
schemaPath: 'https://…#/properties/email', // absolute URI into the SCHEMA
|
|
389
|
+
params: { format: 'email' }, // raw structured values, never prose
|
|
390
|
+
msgid: 'format', // stable catalog key for i18n
|
|
391
|
+
message: 'must match format "email"'
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Two notes for anyone diffing this against another validator's output. The
|
|
396
|
+
data location is `instancePath`, a **JSON Pointer string** — not the dotted
|
|
397
|
+
`dataPath` of Ajv v6, and not an array path. `parseJSONPointerPath` from
|
|
398
|
+
[`@jarenjs/json`](../json/README.md) converts it to the `(string|number)[]`
|
|
399
|
+
path shape that Zod's `issue.path` and most diffing tools use, narrowing
|
|
400
|
+
canonical array indexes to numbers:
|
|
401
|
+
|
|
402
|
+
```javascript
|
|
403
|
+
import { parseJSONPointerPath } from '@jarenjs/json';
|
|
404
|
+
|
|
405
|
+
parseJSONPointerPath('/items/0/id'); // ['items', 0, 'id']
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
And for `additionalProperties: false`, Jaren points `instancePath` at the
|
|
409
|
+
offending member (`/nested/extra`) where Ajv points at the parent object —
|
|
410
|
+
deliberate, and spec-truer.
|
|
411
|
+
|
|
412
|
+
**Collected errors are exhaustive across independent keywords.** Every
|
|
413
|
+
keyword that can fail independently reports its own fault, rather than the
|
|
414
|
+
first failure hiding the rest: keyword groups on one node (`enum` beside
|
|
415
|
+
`minLength`), `minProperties` beside `required`, a numeric bound beside
|
|
416
|
+
`multipleOf`, `minItems` beside `uniqueItems` and beside failing items, every
|
|
417
|
+
`allOf` branch, independent applicator groups, and every absent `required`
|
|
418
|
+
property. Boolean mode still stops at the first failure — that is the whole
|
|
419
|
+
point of it — so the two modes deliberately differ in how much work they do:
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
const contract = {
|
|
423
|
+
type: 'object',
|
|
424
|
+
properties: { slug: { type: 'string', minLength: 3, pattern: '^[a-z]+$' } },
|
|
425
|
+
required: ['name', 'slug'],
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
collecting.compile(contract)({ slug: '!' }).errors;
|
|
429
|
+
// required at '', minLength at '/slug', pattern at '/slug' — three issues
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**A speculative applicator never reports.** An `anyOf`/`oneOf` branch that was
|
|
433
|
+
not the one that matched, the subschema of a `not`, an `if` condition, and a
|
|
434
|
+
`contains` candidate that is not the match are all probes — the document was
|
|
435
|
+
never required to satisfy them, so their failures are rolled back rather than
|
|
436
|
+
handed to your caller. What survives is what genuinely explains the failure:
|
|
437
|
+
if *no* `anyOf` branch matched, every branch's errors are kept, because then
|
|
438
|
+
they are the reason.
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
collecting.compile({
|
|
442
|
+
type: 'string', minLength: 5,
|
|
443
|
+
anyOf: [{ type: 'number' }, { type: 'string' }],
|
|
444
|
+
})('x').errors;
|
|
445
|
+
// [minLength] — not a type error from the number branch that was never required
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
A `required` error points at the **owning object**, not at the absent member
|
|
449
|
+
(there is no location for something that is not there); the missing name is in
|
|
450
|
+
`params.missingProperty`, which is what an adapter appends to build a
|
|
451
|
+
Zod-style path.
|
|
452
|
+
|
|
453
|
+
### String lengths count graphemes by default
|
|
454
|
+
|
|
455
|
+
`useGrapheme` defaults to **`true`**, so `minLength`/`maxLength` count
|
|
456
|
+
user-perceived characters. Most other validators — and the JSON Schema
|
|
457
|
+
specification itself — count UTF-16 code units:
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
const family = '👨👩👧👦'; // 1 grapheme cluster, 11 UTF-16 code units
|
|
461
|
+
|
|
462
|
+
new JarenValidator().compile({ type: 'string', maxLength: 2 })(family);
|
|
463
|
+
// => true (1 grapheme)
|
|
464
|
+
|
|
465
|
+
new JarenValidator({ useGrapheme: false })
|
|
466
|
+
.compile({ type: 'string', maxLength: 2 })(family);
|
|
467
|
+
// => false (11 code units)
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**If you are migrating from a validator that counts code units, set
|
|
471
|
+
`useGrapheme: false`,** or strings containing emoji, combining marks, flags
|
|
472
|
+
or astral-plane characters will silently change validity at the boundaries.
|
|
473
|
+
Grapheme mode is not expensive — ASCII takes a `str.length` fast path and
|
|
474
|
+
most Unicode takes a code-point count; only cluster-forming strings reach
|
|
475
|
+
`Intl.Segmenter` — so the default is about correctness, not speed, and
|
|
476
|
+
switching to it later is a product decision rather than a performance one.
|
|
477
|
+
|
|
478
|
+
### `format` and content assertion follow the draft
|
|
479
|
+
|
|
480
|
+
Both are annotation-only in the drafts that say so, and both take an explicit
|
|
481
|
+
override:
|
|
482
|
+
|
|
483
|
+
- **`format`** asserts through draft 2019-09 and is annotation-only from
|
|
484
|
+
draft 2020-12 on, per spec. It also turns on automatically when the
|
|
485
|
+
schema's meta-schema declares the `format-assertion` vocabulary.
|
|
486
|
+
- **`contentEncoding`/`contentMediaType`** assert through draft-07 and are
|
|
487
|
+
annotation-only from 2019-09 on.
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
const schema = { $schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
491
|
+
type: 'string', format: 'email' };
|
|
492
|
+
|
|
493
|
+
new JarenValidator().addFormats(formats.stringFormats)
|
|
494
|
+
.compile(schema)('nope'); // => true (annotation only)
|
|
495
|
+
|
|
496
|
+
new JarenValidator({ formatAssertion: true }).addFormats(formats.stringFormats)
|
|
497
|
+
.compile(schema)('nope'); // => false (asserted)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Formats are never registered implicitly — and an unregistered one is an error
|
|
501
|
+
|
|
502
|
+
`@jarenjs/validate` does not depend on `@jarenjs/formats`, so a fresh
|
|
503
|
+
validator has an **empty format registry**. Registering the groups you use is
|
|
504
|
+
the whole setup:
|
|
505
|
+
|
|
506
|
+
```javascript
|
|
507
|
+
import * as formats from '@jarenjs/formats';
|
|
508
|
+
|
|
509
|
+
const jaren = new JarenValidator()
|
|
510
|
+
.addFormats(formats.stringFormats) // email, uri, uuid, hostname, iban, ...
|
|
511
|
+
.addFormats(formats.dateTimeFormats) // date-time, date, time, duration
|
|
512
|
+
.addFormats(formats.numberFormats) // int32, double, decimal, ...
|
|
513
|
+
.addFormats(formats.jsonFormats) // json-pointer, json-path, regex, ...
|
|
514
|
+
.addFormats(formats.geoFormats); // geohash, lat-long, ...
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
**An unregistered format name asserts nothing, and by default says nothing** —
|
|
518
|
+
that is the specification's rule ("implementations MUST NOT fail validation or
|
|
519
|
+
cease processing due to an unknown format attribute"), and it is the default
|
|
520
|
+
because a library has to be able to compile a schema it did not write. The
|
|
521
|
+
cost is a real trap: a `format: 'date-time'` you believed was checking
|
|
522
|
+
timestamps, checking nothing, and looking exactly like one that works.
|
|
523
|
+
|
|
524
|
+
**`unknownFormats: 'error'` turns that into a compile-time throw.** Set it for
|
|
525
|
+
schemas you own:
|
|
526
|
+
|
|
527
|
+
```javascript
|
|
528
|
+
new JarenValidator({ unknownFormats: 'error' })
|
|
529
|
+
.compile({ type: 'string', format: 'date-time' });
|
|
530
|
+
// => Error: Unknown format 'date-time': no compiler is registered for it,
|
|
531
|
+
// so this schema would accept every value for that keyword. Register one
|
|
532
|
+
// (addFormats(dateTimeFormats) …), or pass { unknownFormats: 'ignore' } …
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Instance validation is untouched either way — **no data is ever invalidated by
|
|
536
|
+
an unknown format**, so `'error'` stays inside the specification's letter: it
|
|
537
|
+
reports to the schema's *author*, at compile time, where a missing
|
|
538
|
+
registration is a fact about the setup rather than about the data. The
|
|
539
|
+
official suite's `optional/format/unknown.json` (unknown format, assertion
|
|
540
|
+
forced on, everything must validate) passes on the default and is the one
|
|
541
|
+
thing `'error'` would fail, which is why it is opt-in.
|
|
542
|
+
|
|
543
|
+
This repository sets `'error'` for every schema it ships, and a gate
|
|
544
|
+
(`test/validate/our-schema-formats.test.js`) fails if any of them names a
|
|
545
|
+
format `@jarenjs/formats` does not implement. It is worth copying: the
|
|
546
|
+
published `jaren-query` grammar declared `format: "json-path"` from the day it
|
|
547
|
+
was written, nothing that compiled it registered `jsonFormats`, and so no path
|
|
548
|
+
string was ever checked by that keyword. Nothing failed. That is the shape of
|
|
549
|
+
this bug.
|
|
550
|
+
|
|
551
|
+
Where **not** to reach for `'error'`: a schema a user hands you. Refusing to
|
|
552
|
+
compile a stranger's valid schema over a format you happen not to implement is
|
|
553
|
+
your problem presented as theirs — this repo keeps the default for the
|
|
554
|
+
validator playground's input and for project files in the studio, and uses
|
|
555
|
+
`'error'` only for its own grammars. `@jarenjs/ai`'s ledger is the other
|
|
556
|
+
exception: it cannot depend on `@jarenjs/formats` at all, so it stays on the
|
|
557
|
+
default and puts a `pattern` beside the `format` to do the enforcing.
|
|
558
|
+
|
|
559
|
+
Two things `'error'` deliberately leaves alone. It does not fire where
|
|
560
|
+
`format` is annotation-only anyway (draft 2020-12 without the format-assertion
|
|
561
|
+
vocabulary) — nothing is lost there, so there is nothing to report. And
|
|
562
|
+
**meta-schemas** are exempt: every JSON Schema meta-schema declares
|
|
563
|
+
`format: "uri-reference"` on `$id`, and nobody registers formats in order to
|
|
564
|
+
check that a *schema* is well-formed, so `addMetaSchema` compiles with
|
|
565
|
+
`unknownFormats: 'ignore'` regardless.
|
|
566
|
+
|
|
567
|
+
Registering a format with something that is **not a compiler** throws under
|
|
568
|
+
either setting, because that mistake is never intentional:
|
|
569
|
+
|
|
570
|
+
```javascript
|
|
571
|
+
new JarenValidator().addFormats(formats.formatTesters) // testers, not compilers
|
|
572
|
+
.compile({ type: 'string', format: 'email' });
|
|
573
|
+
// => Error: Format 'email' failed to compile: … Register the format COMPILERS
|
|
574
|
+
// (stringFormats, dateTimeFormats, …) rather than the raw testers.
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
`formatTesters` and `stringFormats` are both objects full of functions, so the
|
|
578
|
+
mistake registers cleanly and then checks nothing. Only the **compilers** take
|
|
579
|
+
`(schemaObj, jsonSchema)` and return the per-value validator.
|
|
580
|
+
|
|
581
|
+
Registration never overwrites an existing name, so register your own
|
|
582
|
+
overrides *before* a bundled group if you want them to win.
|
|
583
|
+
|
|
584
|
+
### Recipe: migrating from Zod
|
|
585
|
+
|
|
586
|
+
Zod counts UTF-16 code units, always reports every issue, and always
|
|
587
|
+
asserts formats. This factory reproduces those three answers:
|
|
588
|
+
|
|
589
|
+
```javascript
|
|
590
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
591
|
+
import * as formats from '@jarenjs/formats';
|
|
592
|
+
|
|
593
|
+
export const jaren = new JarenValidator({
|
|
594
|
+
collectErrors: true, // { valid, errors } instead of a boolean
|
|
595
|
+
useGrapheme: false, // UTF-16 code units, like Zod's .min()/.max()
|
|
596
|
+
formatAssertion: true, // assert format even under draft 2020-12
|
|
597
|
+
contentValidation: true, // assert contentEncoding/contentMediaType
|
|
598
|
+
})
|
|
599
|
+
.addFormats(formats.stringFormats)
|
|
600
|
+
.addFormats(formats.dateTimeFormats);
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
What this recipe does **not** give you is Zod's output normalization: a
|
|
604
|
+
compiled validator is a pure predicate and never modifies its input. That
|
|
605
|
+
boundary does not move — but the normalization itself ships beside it, as a
|
|
606
|
+
separately compiled pass. See [Normalization](#normalization) below.
|
|
607
|
+
|
|
608
|
+
## Normalization
|
|
609
|
+
|
|
610
|
+
Validation answers a question; it does not change your data. When you need
|
|
611
|
+
the *normalized output* that a parse-and-transform library returns —
|
|
612
|
+
materialized defaults, decoded transport values, stripped unknown members —
|
|
613
|
+
compile a normalizer from the same schema and run it first:
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
import { compileNormalizer } from '@jarenjs/validate/normalize';
|
|
617
|
+
|
|
618
|
+
const normalize = compileNormalizer(schema, {
|
|
619
|
+
useDefaults: true, // fill absent properties from `default`, recursively
|
|
620
|
+
removeAdditional: true, // drop members the schema forbids
|
|
621
|
+
coerceTypes: true, // '9000' -> 9000 where the schema says integer
|
|
622
|
+
trimStrings: true, // ' jaren ' -> 'jaren'
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const shaped = normalize(input); // a NEW value; `input` is untouched
|
|
626
|
+
const result = validate(shaped);
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Every option is **off by default** — each one changes what your data means,
|
|
630
|
+
so each is a decision you make rather than one you inherit.
|
|
631
|
+
`compileNormalizer(schema)` with no options is the identity.
|
|
632
|
+
|
|
633
|
+
### Per-field control, not just a global switch
|
|
634
|
+
|
|
635
|
+
`useDefaults`, `coerceTypes` and `trimStrings` each accept a **predicate**
|
|
636
|
+
`(schemaNode) => boolean` in place of a boolean. It runs once per node during
|
|
637
|
+
compilation, so it decides per field and costs nothing at runtime:
|
|
638
|
+
|
|
639
|
+
```javascript
|
|
640
|
+
const normalize = compileNormalizer(schema, {
|
|
641
|
+
trimStrings: (node) => node['x-trim'] === true,
|
|
642
|
+
});
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
This matters more than it looks. A contract typically trims a handful of its
|
|
646
|
+
string fields and must leave the rest byte-for-byte as supplied — a timezone
|
|
647
|
+
name, a deliberately padded identifier, a field whose whitespace is data.
|
|
648
|
+
`trimStrings: true` would quietly rewrite all of them. The predicate reads
|
|
649
|
+
whatever you put in the schema (an `x-` annotation, a `format`, a name
|
|
650
|
+
pattern), which keeps the policy next to the field it governs and portable
|
|
651
|
+
with the schema.
|
|
652
|
+
|
|
653
|
+
### It never mutates, and it shares what it can
|
|
654
|
+
|
|
655
|
+
Ajv's `useDefaults`/`coerceTypes` write into the document you hand them.
|
|
656
|
+
This does not: it is a copy-on-write walk, so the input is exactly as it was
|
|
657
|
+
afterwards — you can normalize a frozen document, or keep the original as an
|
|
658
|
+
audit record, without defensive copying.
|
|
659
|
+
|
|
660
|
+
The other half of that design is identity. A subtree that needs no change is
|
|
661
|
+
returned by reference, and a document that needs no change at all returns the
|
|
662
|
+
input itself:
|
|
663
|
+
|
|
664
|
+
```javascript
|
|
665
|
+
normalize(alreadyClean) === alreadyClean; // true
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
So a no-op costs nothing, and downstream memoization keyed on identity keeps
|
|
669
|
+
working.
|
|
670
|
+
|
|
671
|
+
### What it walks, and what it deliberately does not
|
|
672
|
+
|
|
673
|
+
Walked: `properties`, `patternProperties`, `additionalProperties`,
|
|
674
|
+
`items`/`prefixItems`/`additionalItems` (both tuple spellings), same-document
|
|
675
|
+
`$ref` including recursive ones, and `allOf` — whose branches compose, with
|
|
676
|
+
stripping disabled inside them because one branch cannot know what a sibling
|
|
677
|
+
declares.
|
|
678
|
+
|
|
679
|
+
Member schemas **compose the way JSON Schema says they do**: a member covered
|
|
680
|
+
by `properties` *and* by one or more matching `patternProperties` is
|
|
681
|
+
normalized by every one of them, in that order, and `additionalProperties`
|
|
682
|
+
applies only to a member nothing else covered. A materialized `default` runs
|
|
683
|
+
through its own property's normalizer too, so a defaulted `{ port: '8080' }`
|
|
684
|
+
is shaped exactly like a supplied one rather than keeping its string.
|
|
685
|
+
|
|
686
|
+
Not walked: `anyOf`, `oneOf`, `if`/`then`/`else`, `not`. Which branch applies
|
|
687
|
+
is only known after validating, and normalizing under one branch can change
|
|
688
|
+
which branch validates — so guessing would be worse than declining. There is
|
|
689
|
+
also no transform hook: an arbitrary transform is application code, not
|
|
690
|
+
schema semantics, and belongs on your side of the boundary.
|
|
691
|
+
|
|
692
|
+
### The coercion table
|
|
693
|
+
|
|
694
|
+
Coercion exists to decode transport encodings — query strings, form fields,
|
|
695
|
+
environment variables, CSV cells — where everything arrives as a string. It
|
|
696
|
+
is conservative on purpose: a value it cannot convert unambiguously is passed
|
|
697
|
+
through unchanged, so validation reports the type error instead of the
|
|
698
|
+
normalizer hiding it.
|
|
699
|
+
|
|
700
|
+
| Declared `type` | Converted | Left alone |
|
|
701
|
+
|---|---|---|
|
|
702
|
+
| `number` | a string that is exactly a JSON number (`'1e5'`, `'-0.5'`) | `'0x10'`, `'1_000'`, `''`, `'Infinity'` |
|
|
703
|
+
| `integer` | as `number`, when the result is integral | `'4.5'` |
|
|
704
|
+
| `boolean` | `'true'`, `'false'` | `'yes'`, `'1'`, `0` |
|
|
705
|
+
| `string` | finite numbers and booleans | objects, arrays, `null` |
|
|
706
|
+
| `null` | `'null'` | `''`, `0`, `false` |
|
|
707
|
+
|
|
708
|
+
A union `type` (`['string', 'number']`) gives no single target, so coercion is
|
|
709
|
+
skipped rather than guessed. Trimming runs before coercion, so `' 42 '`
|
|
710
|
+
decodes for an integer field.
|
|
711
|
+
|
|
712
|
+
## Error messages & i18n
|
|
713
|
+
|
|
714
|
+
Every collected error carries a stable message key (`msgid`) and raw
|
|
715
|
+
structured `params` next to its rendered `message` — prose is produced at
|
|
716
|
+
report time from a **catalog** (a plain object of closures), never on the
|
|
717
|
+
validation hot path. The normative spec is
|
|
718
|
+
[ERROR-MESSAGES.md](./docs/ERROR-MESSAGES.md).
|
|
719
|
+
|
|
720
|
+
### The `errorMessage` keyword
|
|
721
|
+
|
|
722
|
+
Author-supplied messages that override *text*, never structure (no error
|
|
723
|
+
aggregation or removal — the deliberate divergence from ajv-errors, whose
|
|
724
|
+
two official plugins are mutually incompatible). Registered at schema
|
|
725
|
+
compile time, resolved only over the failed set at report time — zero
|
|
726
|
+
validation-time cost.
|
|
727
|
+
|
|
728
|
+
```javascript
|
|
729
|
+
// string form: covers the node AND its subtree (quiet oneOf noise)
|
|
730
|
+
{ "type": "string", "minLength": 8, "errorMessage": "Use at least 8 characters" }
|
|
731
|
+
|
|
732
|
+
// map form: per failing keyword, '_' as the node catch-all
|
|
733
|
+
{ "type": "integer", "minimum": 18,
|
|
734
|
+
"errorMessage": { "minimum": "Must be an adult", "_": "Invalid age" } }
|
|
735
|
+
|
|
736
|
+
// required: per missing property
|
|
737
|
+
{ "required": ["vatId", "name"],
|
|
738
|
+
"errorMessage": { "required": { "vatId": "VAT id is required for business accounts" } } }
|
|
739
|
+
|
|
740
|
+
// $query: EBV-false default and per runtime code
|
|
741
|
+
{ "$query": { "$le": ["$.start", "$.end"] },
|
|
742
|
+
"errorMessage": { "$query": {
|
|
743
|
+
"default": "start must not be after end",
|
|
744
|
+
"JQ2003": "start/end must be single values" } } }
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
Templates interpolate params: `"errorMessage": "needs {limit} characters"`.
|
|
748
|
+
|
|
749
|
+
### `$msgid` — translatable authored messages
|
|
750
|
+
|
|
751
|
+
An `errorMessage` (or forms `x-form.message`) may be a **MessageSpec**
|
|
752
|
+
object pointing into the catalog space instead of inline text — that keeps
|
|
753
|
+
schema-authored messages translatable:
|
|
754
|
+
|
|
755
|
+
```javascript
|
|
756
|
+
{ "type": "number",
|
|
757
|
+
"errorMessage": { "type": {
|
|
758
|
+
"$msgid": "checkout.total-invalid", // catalog key
|
|
759
|
+
"message": "Total must be a number" // fallback when no catalog covers it
|
|
760
|
+
} } }
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Locale packs
|
|
764
|
+
|
|
765
|
+
```javascript
|
|
766
|
+
import { JarenValidator, compileMessageCatalog, localizeErrors } from '@jarenjs/validate';
|
|
767
|
+
import { nl } from '@jarenjs/locales';
|
|
768
|
+
|
|
769
|
+
const catalog = compileMessageCatalog(nl);
|
|
770
|
+
const validator = new JarenValidator({ collectErrors: true });
|
|
771
|
+
const validate = validator.compile({ type: 'string', minLength: 2 });
|
|
772
|
+
|
|
773
|
+
const result = validate('x'); // English messages
|
|
774
|
+
localizeErrors(result.errors, catalog); // Dutch, re-rendered from msgid + params
|
|
775
|
+
// 'mag niet minder dan 2 tekens bevatten'
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
The locale is chosen at **report time**; switching locale never recompiles
|
|
779
|
+
anything. Catalog entries are plain functions, so packs use the platform's
|
|
780
|
+
`Intl.PluralRules`/`Intl.NumberFormat` — see
|
|
781
|
+
[`@jarenjs/locales`](../locales/README.md) for the pack-authoring guide.
|
|
782
|
+
|
|
783
|
+
### `messages: false`
|
|
784
|
+
|
|
785
|
+
For applications that render exclusively through `localizeErrors` (or
|
|
786
|
+
their own resolver), skip English rendering entirely:
|
|
787
|
+
|
|
788
|
+
```javascript
|
|
789
|
+
const validator = new JarenValidator({ collectErrors: true, messages: false });
|
|
790
|
+
// errors arrive with message: '', params and msgid still set
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
## Development
|
|
794
|
+
|
|
795
|
+
Unit tests live in `test/validate/` at the repository root (`npm run test:validate`). Performance against Ajv is measured over the official test suite with the [benchmark workspace](../../benchmark/README.md) (`node benchmark/profiler.js --profile-all`), which also houses the test-failure debugger, coverage and call-graph tools.
|
|
796
|
+
|
|
797
|
+
This package's internals — the four-phase compile pipeline, ref flattening, annotation tracking, dynamic scope — are described in its own [ARCHITECTURE](./ARCHITECTURE.md) document. For practical usage recipes (options, lightweight setups, custom formats, pitfalls) see the repository [HOWTO](../../docs/HOWTO.md); for the monorepo picture see the repository [README](../../README.md) and [ARCHITECTURE](../../docs/ARCHITECTURE.md); for what is planned next see the [ROADMAP](../../docs/ROADMAP.md).
|