@jarenjs/validate 0.9.2 → 0.34.2

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.
@@ -14,7 +14,48 @@ export declare function isOfSchemaType(schema: any, type: any): boolean;
14
14
  export declare function hasSchemaRef(schema: any): boolean;
15
15
  export declare function hasSchemaRecursiveRef(schema: any): boolean;
16
16
  export declare function hasSchemaDynamicRef(schema: any): boolean;
17
+ /**
18
+ * Whether a sibling keyword of unevaluatedProperties already evaluates every
19
+ * property of the instance. additionalProperties (boolean or schema) applies
20
+ * to each property not matched by properties/patternProperties, so once it
21
+ * has passed no property is left unevaluated.
22
+ * @param {object} schema - The schema holding the unevaluatedProperties keyword
23
+ * @returns {boolean} True when the unevaluatedProperties check can never match
24
+ */
25
+ export declare function hasUnevaluatedPropertiesCoverage(schema: object): boolean;
26
+ /**
27
+ * Whether a sibling keyword of unevaluatedItems already evaluates every item
28
+ * of the instance: a uniform items schema (boolean or object) covers all
29
+ * items beyond any prefixItems, and a tuple-form items with additionalItems
30
+ * covers the items beyond the tuple.
31
+ * @param {object} schema - The schema holding the unevaluatedItems keyword
32
+ * @returns {boolean} True when the unevaluatedItems check can never match
33
+ */
34
+ export declare function hasUnevaluatedItemsCoverage(schema: object): boolean;
17
35
  export declare function createIsSchemaTypeHandler(type: any, isStrict?: boolean): typeof isBooleanType | typeof isRegExpType | undefined;
36
+ /**
37
+ * The fallback resolver of the data-reference keywords (`data`, `$data`):
38
+ * a ref that fails the strict compile keeps the lax keyword semantics, so
39
+ * it resolves as not-found and the keyword asserts nothing.
40
+ * @returns {any} the JSON Pointer not-found sentinel
41
+ */
42
+ export declare const resolveNothing: () => any;
43
+ /**
44
+ * Build the keyword validators the two data-reference keywords share
45
+ * verbatim. The `data` keyword (json-everything, absolute + relative
46
+ * pointers via `compileDataRef`) and the Ajv-style `$data` keyword
47
+ * (relative pointers only) differ ONLY in which pointer compiler
48
+ * resolves a ref, so each module passes its own `compileRefResolver`
49
+ * and gets the same fifteen compilers back.
50
+ *
51
+ * Every validator follows one lax contract: a data instance outside the
52
+ * keyword's type, an unresolvable ref, or a resolved constraint of the
53
+ * wrong type asserts nothing.
54
+ *
55
+ * @param {(ref: string) => (dataRoot: any, dataPath: string) => any} compileRefResolver
56
+ * @returns {Record<string, (schemaObj: object, ref: string) => ((data: any, dataPath: string, dataRoot: any) => boolean) | undefined>}
57
+ */
58
+ export declare function createDataRefCompilers(compileRefResolver: (ref: string) => (dataRoot: any, dataPath: string) => any): Record<string, (schemaObj: object, ref: string) => ((data: any, dataPath: string, dataRoot: any) => boolean) | undefined>;
18
59
  /**
19
60
  * Records which properties (string keys) and items (numeric indexes) of a
20
61
  * data instance were successfully evaluated during validation, so that
@@ -40,6 +81,23 @@ export declare class EvalLog {
40
81
  /** @returns {boolean} True when item `index` of `data` was evaluated at or after `from` (-1 entries cover all items) */
41
82
  hasItem(data: any, index: any, from: any): boolean;
42
83
  }
84
+ /**
85
+ * Combine INDEPENDENT keyword validators without short-circuiting.
86
+ *
87
+ * `a(...) && b(...)` is the right composition in boolean mode: the answer is
88
+ * known at the first failure and nothing is gained by continuing. When errors
89
+ * are recorded it is wrong, because each validator is the only thing that can
90
+ * report its own fault, so the first failure hides every sibling's. This runs
91
+ * all of them and ANDs the results — the boolean answer is identical, the
92
+ * error list is complete.
93
+ *
94
+ * Only use it where the validators genuinely are independent. A precondition
95
+ * (a type guard before a length check) must keep its short-circuit: running
96
+ * past it is meaningless at best and throws at worst.
97
+ * @param {Function[]} validators - Independent validators, in report order
98
+ * @returns {Function} A validator that runs every one of them
99
+ */
100
+ export declare function combineIndependent(validators: Function[]): Function;
43
101
  export declare class ValidationResult {
44
102
  match: boolean;
45
103
  errors: number;
@@ -0,0 +1,251 @@
1
+ # Jaren Error Messages & i18n
2
+
3
+ **Status: normative.** This document specifies the error record shape, the
4
+ message-key space, the `MessageSpec` value type, the `errorMessage`
5
+ keyword, catalogs, and the resolution precedence chain implemented by
6
+ `@jarenjs/validate` (src/messages.js) and mirrored by `@jarenjs/forms`
7
+ (src/messages.js). Locale packs live in `@jarenjs/locales`.
8
+
9
+ The design premise: **the validator and the forms engine are not the place
10
+ where prose is born.** Every failure is identified by a stable message key
11
+ (`msgid`) plus raw structured `params`; human text is produced only at
12
+ report/render time, over the already-failed set, by a locale catalog. The
13
+ validation hot path (boolean mode, the `skipErrors` no-op handlers, the
14
+ fast-path node compilers) is untouched: switching locale never recompiles
15
+ a validator or a form model's rules.
16
+
17
+ ---
18
+
19
+ ## 1. The error record
20
+
21
+ Collect-mode validation (`collectErrors: true`) returns
22
+ `{ valid, errors: ValidationError[] }` where every error is:
23
+
24
+ | member | type | meaning |
25
+ |---|---|---|
26
+ | `keyword` | string | the failed keyword (`type`, `required`, `$query`, ...) |
27
+ | `instancePath` | string | RFC 6901 JSON Pointer to the failing data location |
28
+ | `schemaPath` | string | the schema location (the compiled node's URI path) |
29
+ | `params` | object | keyword-specific raw parameters (section 2) |
30
+ | `msgid` | string | the resolved message key (section 2) |
31
+ | `message` | string | human text — `''` when `messages: false` |
32
+
33
+ `toJSON()` serializes exactly these six members.
34
+
35
+ `msgid` resolution: the matched `errorMessage` spec's `$msgid` if any,
36
+ else `params.code` if present (the `$query` runtime codes), else the
37
+ keyword.
38
+
39
+ With the validator option `messages: false`, conversion skips message
40
+ rendering entirely (`message: ''`; `params` and `msgid` still set) — the
41
+ fast path for applications that render exclusively through
42
+ `localizeErrors` or their own resolver.
43
+
44
+ ### The handler-contract invariant (internal)
45
+
46
+ Every internal error handler call site obeys: normal handlers
47
+ `addError(data, dataPath, ...extra)`, keyed handlers
48
+ `addKeyedError(dataKey, data, dataPath, ...extra)` — the first meta
49
+ argument is ALWAYS the instance data path, so `instancePath` is a straight
50
+ read. A charCode guard keeps a non-pointer value from ever becoming a
51
+ wrong path (it yields `''`).
52
+
53
+ ### Deliberate `additionalProperties` divergence from ajv
54
+
55
+ An `additionalProperties: false` failure reports `instancePath` at the
56
+ **offending member** (`/nested/extra`), not at the parent object
57
+ (`/nested`). The object compiler passes the child path
58
+ (`${dataPath}/${dataKey}`) as the error's data path, so the pointer lands
59
+ on the disallowed property itself. This is spec-correct (the failing
60
+ location *is* that property) and friendlier for a UI that highlights the
61
+ field. ajv reports the parent object's path with the property name in
62
+ `params.additionalProperty`; Jaren carries the same `additionalProperty`
63
+ param, so a consumer diffing error sets against ajv should treat the
64
+ `instancePath` difference as intentional, not a bug.
65
+
66
+ ## 2. The message-key registry
67
+
68
+ One flat namespace:
69
+
70
+ | Producer | Keys | Params carried |
71
+ |---|---|---|
72
+ | validate keywords | the keyword itself: `type`, `required`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `minLength`, `maxLength`, `pattern`, `additionalProperties`, `minProperties`, `maxProperties`, `minItems`, `maxItems`, `uniqueItems`, `contains`, `items`, `allOf`, `anyOf`, `oneOf`, `not`, `format`, `if`, `then`, `else`, `false schema`, `$query` | `type`/`types`; `missingProperty`; `limit` + `comparison`; `multipleOf`; `pattern`; `additionalProperty`; `format` |
73
+ | query runtime via `$query` | the `JQ2xxx` code (`JQ2001`, `JQ2003`, ...) | `{ code, docPath }` |
74
+ | forms field checks | `form/` + the emitted keyword: `form/required`, `form/type`, `form/const`, `form/enum`, `form/minLength`, `form/maxLength`, `form/pattern`, `form/format`, `form/minimum`, `form/maximum`, `form/exclusiveMinimum`, `form/exclusiveMaximum`, `form/multipleOf`, `form/minItems`, `form/maxItems`, `form/uniqueItems`, `form/minProperties`, `form/maxProperties` | per branch: `limit`, `len`, `type`, `format`, `enumValues`, `constValue`, `pattern`, `multipleOf` |
75
+ | forms rules | `x-form/assert` (the default), else the author's `$msgid` | `{ pointer, ...spec.params }` |
76
+ | applications | any `$msgid` they invent — recommend dotted names (`checkout.total-too-low`) | error params + spec params |
77
+
78
+ Keywords without a built-in English entry (`const`, `enum`,
79
+ `dependentRequired`, `dependencies`, `unevaluatedProperties`, ...) render
80
+ the generic `validation failed for keyword '<keyword>'`; their `msgid` is
81
+ still the keyword, so a catalog (or an application) may cover them.
82
+
83
+ ## 3. MessageSpec
84
+
85
+ The value type used identically by `errorMessage` (validate),
86
+ `x-form.message` (forms), and anywhere else a message is authored:
87
+
88
+ ```
89
+ MessageSpec = string // inline template (author's language)
90
+ | { "$msgid": string, // catalog key to resolve at render time
91
+ "message"?: string, // inline template fallback on catalog miss
92
+ "params"?: object } // merged OVER the error's params
93
+ ```
94
+
95
+ The object form requires `$msgid` and/or `message`; any other member is a
96
+ compile-time error. Spec `params` merge over the error's params **into the
97
+ error record**, so post-hoc localization sees them too.
98
+
99
+ ### Template syntax
100
+
101
+ Applies to inline templates and to string-valued catalog entries:
102
+
103
+ - `{name}` substitutes the merged params member `name` — `String(v)` for
104
+ primitives, `JSON.stringify(v)` otherwise;
105
+ - an unknown name leaves the placeholder literally (debuggability);
106
+ - `{{` escapes a literal `{`.
107
+
108
+ Templates compile ONCE into a closure (`compileMessageTemplate`) — the
109
+ two-stage house rule applies to messages too. There is **no pointer/data
110
+ interpolation in v1** (`${/foo}` ajv-style is a roadmap follow-up; params
111
+ already carry the relevant values).
112
+
113
+ ## 4. Catalogs
114
+
115
+ A catalog is a plain flat object:
116
+ `{ [key]: (params, error) => string | templateString }`.
117
+ `compileMessageCatalog(catalogLike)` returns a functions-only frozen copy
118
+ (template strings compiled).
119
+
120
+ English catalogs are **built in**: validate's `messagesEn`
121
+ (src/messages.js) and forms' `formsMessagesEn` (src/messages.js) — neither
122
+ package gains a dependency. Non-English packs live in `@jarenjs/locales`
123
+ (`nl`, `fr`, `es`, `pt`, `de`, `ja`, `ko`, `zhTW`, `ru`, `tr`, `ar` —
124
+ each also a subpath export, e.g. `@jarenjs/locales/fr`,
125
+ `@jarenjs/locales/zh-tw`) and must have key parity with the built-in
126
+ English (enforced by repo tests).
127
+
128
+ ### Pack authoring (globalization mechanics)
129
+
130
+ Catalog entries are functions precisely so packs can use the platform:
131
+
132
+ - `Intl.PluralRules` for plural category selection ("1 teken" /
133
+ "2 tekens" — see the `nl` pack's `minLength`),
134
+ - `Intl.NumberFormat` for `{limit}`-style numbers,
135
+ - `Intl.ListFormat` for enum lists.
136
+
137
+ Hold these as module-level singletons (allocation discipline). **Bidi:**
138
+ packs targeting RTL scripts should isolate interpolated user values with
139
+ FSI/PDI (U+2068/U+2069) — the pack author's call, not core's. A pack
140
+ depends on nothing outside the Jaren suite, and inside it on nothing but
141
+ `@jarenjs/core` — never on `@jarenjs/validate` or `@jarenjs/forms`, so
142
+ either consumer can serve any pack.
143
+
144
+ ## 5. Resolution precedence
145
+
146
+ Implemented in validate's conversion (`convertInternalErrors`); forms
147
+ mirrors the tail of the chain:
148
+
149
+ 1. the nearest `errorMessage` spec for the error (section 6 matching);
150
+ 2. if the spec has `$msgid`: active catalog → built-in English catalog →
151
+ the spec's inline `message` template;
152
+ 3. if the spec is inline (string / `message`-only): render it;
153
+ 4. no spec: active catalog[`msgid`] → built-in English[`msgid`] →
154
+ catalog[`keyword`] → English[`keyword`] (so uncovered `JQ*` codes still
155
+ say something) → `validation failed for keyword '<keyword>'`.
156
+
157
+ At conversion time the "active catalog" is the built-in English; other
158
+ locales enter through `localizeErrors` / `renderErrorMessage` (section 7)
159
+ or, in forms, through the `catalog` parameters.
160
+
161
+ ## 6. The `errorMessage` keyword
162
+
163
+ **Overrides text, never structure.** Deliberate divergences from
164
+ ajv-errors:
165
+
166
+ - errors are never removed, merged, or aggregated;
167
+ - no synthetic `keyword: "errorMessage"` error is created;
168
+ - originals are never moved into `params.errors`;
169
+ - no `${/pointer}` data interpolation (v1);
170
+ - no `properties`/`items` map forms — the subtree prefix rule covers what
171
+ those express (roadmap follow-up if demand appears).
172
+
173
+ An `errorMessage` spec only changes what `message` (and `msgid`) say on
174
+ the errors it matches.
175
+
176
+ ### Grammar
177
+
178
+ Validated at schema compile time; malformed specs throw with the schema
179
+ path.
180
+
181
+ ```jsonc
182
+ "errorMessage": MessageSpec // string form: whole subtree
183
+ "errorMessage": { // map form
184
+ "minLength": MessageSpec, // per failing keyword, this node
185
+ "required": MessageSpec // all required failures here
186
+ | { "vatId": MessageSpec, ... }, // or per missing property
187
+ "$query": MessageSpec // EBV-false and any runtime code
188
+ | { "default": MessageSpec, // EBV-false
189
+ "JQ2001": MessageSpec, ... }, // per runtime code
190
+ "_": MessageSpec // node-level catch-all
191
+ }
192
+ ```
193
+
194
+ ### Matching
195
+
196
+ The compiler registers each spec by schema location on the compilation
197
+ root — **no validator closure is emitted**; the keyword contributes zero
198
+ validation-time work and never knocks a node off the fast paths. At
199
+ report time:
200
+
201
+ - candidate nodes are the registered locations that equal the error's
202
+ `schemaPath` or are a segment-aware prefix of it (`/foo` never matches
203
+ `/foobar`); the longest prefix is tried first;
204
+ - **at the error's own node**: keyword-map entry (with `required`
205
+ per-property matching on `params.missingProperty`, `$query` per-code on
206
+ `params.code`, `default` on its absence) > `_` > string form;
207
+ - **at an ancestor**: only the string form applies (it covers the
208
+ subtree — this is what lets one string on a `oneOf` replace the branch
209
+ noise);
210
+ - no match at the nearest node falls through to farther ancestors.
211
+
212
+ ## 7. Post-hoc localization
213
+
214
+ ```js
215
+ import { JarenValidator, compileMessageCatalog, localizeErrors } from '@jarenjs/validate';
216
+ import { nl } from '@jarenjs/locales';
217
+
218
+ const catalog = compileMessageCatalog(nl);
219
+ const result = validate(data); // English messages
220
+ localizeErrors(result.errors, catalog); // Dutch messages, same array
221
+ ```
222
+
223
+ `localizeErrors(errors, catalog)` re-renders `message` on each error from
224
+ `msgid` + `params` through the given compiled catalog with built-in
225
+ English fallback. Contract details:
226
+
227
+ - **inline schema-authored text without `$msgid` is single-language by
228
+ definition and is NOT re-rendered** — that is why `$msgid` exists;
229
+ - an error whose `msgid` resolves in no catalog keeps its current message
230
+ (e.g. a custom `$msgid`'s inline fallback text);
231
+ - `renderErrorMessage(error, catalog?)` is the single-error form of the
232
+ same chain (section 5 step 4).
233
+
234
+ Forms renders eagerly (failure-only, cheap, keeps UI consumers simple)
235
+ but through the same catalog contract: `validateField` /
236
+ `validateAllFields` / `evaluateFormRules` accept an optional compiled
237
+ catalog, default English, and every `FieldError` carries
238
+ `{ keyword, params, msgid, message }` so consumers can re-render.
239
+
240
+ ## 8. Why not MessageFormat 2
241
+
242
+ `Intl.MessageFormat` (MessageFormat 2) is TC39 Stage 2 and stalled,
243
+ shipping in no runtime; adopting it would mean a runtime dependency or a
244
+ homegrown MF2 engine for pluralization the platform already provides
245
+ through `Intl.PluralRules`. Catalogs-as-functions cover the same ground
246
+ with no runtime dependency beyond the platform, and with full generality.
247
+
248
+ **Revisit trigger:** Intl.MessageFormat reaching TC39 Stage 3 or shipping
249
+ in a major runtime. At that point, MF2 syntax could become a supported
250
+ catalog *entry format* (compiled by `compileMessageCatalog`) without
251
+ changing the catalog contract.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/validate",
3
3
  "private": false,
4
- "version": "0.9.2",
4
+ "version": "0.34.2",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
@@ -15,11 +15,16 @@
15
15
  "types": "./dist/types/query.d.ts",
16
16
  "default": "./src/query.js"
17
17
  },
18
+ "./normalize": {
19
+ "types": "./dist/types/normalize.d.ts",
20
+ "default": "./src/normalize.js"
21
+ },
18
22
  "./package.json": "./package.json"
19
23
  },
20
24
  "files": [
21
25
  "dist/types/",
22
26
  "src/",
27
+ "docs/",
23
28
  "ARCHITECTURE.md"
24
29
  ],
25
30
  "description": "Jaren is a JavaScript JSON Schema Validator",
@@ -31,7 +36,7 @@
31
36
  },
32
37
  "license": "MIT",
33
38
  "engines": {
34
- "node": ">=22"
39
+ "node": ">=24"
35
40
  },
36
41
  "publishConfig": {
37
42
  "access": "public",
@@ -50,7 +55,7 @@
50
55
  "prepack": "npm run build:types"
51
56
  },
52
57
  "dependencies": {
53
- "@jarenjs/core": "^0.9.2",
54
- "@jarenjs/json": "^0.9.2"
58
+ "@jarenjs/core": "^0.34.2",
59
+ "@jarenjs/json": "^0.34.2"
55
60
  }
56
61
  }
package/src/array.js CHANGED
@@ -225,22 +225,29 @@ function compileItemValidator(schemaObj, itemSchema, key, index) {
225
225
  if (targetObj && targetObj.validate) {
226
226
  return targetObj.validate;
227
227
  }
228
- } catch (e) {
228
+ } catch (_e) {
229
229
  // Fall through to default handling
230
230
  }
231
231
  }
232
232
  }
233
233
  }
234
234
 
235
+ // The two fast paths below return bare predicates with no error handler,
236
+ // so a failing item reports nothing of its own and the caller can only
237
+ // aggregate one error at the array path. When errors are recorded, fall
238
+ // through to full compilation so each failing item yields its own error
239
+ // at its own instancePath, matching the multi-keyword path.
240
+ const stopAtFirst = schemaObj.root.options.skipErrors;
241
+
235
242
  // Fast path: type-only schema (most common case) - check property directly first
236
- if (itemSchema.type !== undefined && Object.keys(itemSchema).length === 1) {
243
+ if (stopAtFirst && itemSchema.type !== undefined && Object.keys(itemSchema).length === 1) {
237
244
  return compileTypeOnlyValidator(itemSchema.type);
238
245
  }
239
246
 
240
247
  // Fast path: required-only schema - check property directly first
241
- if (itemSchema.required !== undefined && Object.keys(itemSchema).length === 1) {
248
+ if (stopAtFirst && itemSchema.required !== undefined && Object.keys(itemSchema).length === 1) {
242
249
  const required = itemSchema.required;
243
- return function validateRequiredOnly(data, dataPath, dataRoot) {
250
+ return function validateRequiredOnly(data, _dataPath, _dataRoot) {
244
251
  // Required properties only apply to objects, not arrays or other types
245
252
  if (typeof data !== 'object' || data === null || Array.isArray(data)) return true;
246
253
  for (let i = 0; i < required.length; i++) {
@@ -337,11 +344,22 @@ export function compileArrayPrimitives(schemaObj, jsonSchema) {
337
344
  const isMinItems = minItems || trueThat;
338
345
  const isMaxItems = maxItems || trueThat;
339
346
 
340
- return function validateArrayPrimitives(data, dataPath) {
347
+ if (schemaObj.options.skipErrors) {
348
+ return function validateArrayPrimitives(data, dataPath) {
349
+ const len = data.length;
350
+ return isMinItems(len, dataPath)
351
+ && isMaxItems(len, dataPath)
352
+ && uniqueItems(data, dataPath);
353
+ };
354
+ }
355
+
356
+ // Length and uniqueness are independent: a short array can also contain
357
+ // duplicates, and a caller fixing one wants to hear about the other.
358
+ return function validateArrayPrimitivesAll(data, dataPath) {
341
359
  const len = data.length;
342
- return isMinItems(len, dataPath)
343
- && isMaxItems(len, dataPath)
344
- && uniqueItems(data, dataPath);
360
+ let valid = isMinItems(len, dataPath);
361
+ valid = isMaxItems(len, dataPath) && valid;
362
+ return uniqueItems(data, dataPath) && valid;
345
363
  };
346
364
  }
347
365
 
@@ -392,7 +410,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
392
410
 
393
411
  // Wrap single-item validator with index loop
394
412
  const itemValidator = validateItem;
395
- validateItem = function validateSingleItemSchema(data, dataPath, dataRoot, i) {
413
+ validateItem = function validateSingleItemSchema(data, dataPath, dataRoot, _i) {
396
414
  return itemValidator(data, dataPath, dataRoot);
397
415
  };
398
416
  } else if (items === true) {
@@ -421,7 +439,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
421
439
  // items: true evaluates every item, which matters when annotations
422
440
  // are tracked for unevaluatedItems.
423
441
  if (track) {
424
- return function validateArrayItemsTrue(data, dataPath, dataRoot) {
442
+ return function validateArrayItemsTrue(data, _dataPath, _dataRoot) {
425
443
  root.evalLog.add(data, -1);
426
444
  return true;
427
445
  };
@@ -459,7 +477,9 @@ function compileArrayChildren(schemaObj, jsonSchema) {
459
477
  return function validateArrayContainsOnly(data, dataPath) {
460
478
  const len = resolveLength(data.length);
461
479
  const arr = data;
462
-
480
+ // Each element is a CANDIDATE probe: the array only has to contain a
481
+ // match, so an element that is not one has done nothing wrong.
482
+ const errors = root.errorMark();
463
483
  let contains = 0;
464
484
  for (let i = 0; i < len; ++i) {
465
485
  if (validator(arr[i], dataPath) === true) {
@@ -467,6 +487,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
467
487
  if (trackContains) root.evalLog.add(data, i);
468
488
  }
469
489
  }
490
+ root.rollbackErrors(errors);
470
491
  return validateMinMax(contains, dataPath);
471
492
  };
472
493
  }
@@ -475,6 +496,7 @@ function compileArrayChildren(schemaObj, jsonSchema) {
475
496
  const itemValidator = validateItem;
476
497
  const containsValidator = validateContains;
477
498
 
499
+ const stopAtFirstContains = schemaObj.options.skipErrors;
478
500
  return function validateArrayChildren(data, dataPath, dataRoot) {
479
501
  const len = resolveLength(data.length);
480
502
  const arr = data;
@@ -491,13 +513,18 @@ function compileArrayChildren(schemaObj, jsonSchema) {
491
513
  else if (track && i < evalLimit) {
492
514
  root.evalLog.add(data, i);
493
515
  }
516
+ // A contains candidate is a probe: not matching is not a fault.
517
+ const containsMark = root.errorMark();
494
518
  if (containsValidator(obj, dataPath, dataRoot) === true) {
495
519
  contains++;
496
520
  if (trackContains) root.evalLog.add(data, i);
497
521
  }
522
+ root.rollbackErrors(containsMark);
498
523
  }
499
- return invalid === 0
500
- && validateMinMax(contains, dataPath);
524
+ // Failing items and the contains count are independent tallies.
525
+ const itemsOk = invalid === 0;
526
+ if (stopAtFirstContains && !itemsOk) return false;
527
+ return validateMinMax(contains, dataPath) && itemsOk;
501
528
  };
502
529
  }
503
530
 
@@ -538,12 +565,21 @@ export function compileArraySchema(schemaObj, jsonSchema) {
538
565
  if (parts.length === 2) {
539
566
  const first = parts[0];
540
567
  const second = parts[1];
541
- return function validateArraySchemaDouble(data, dataPath, dataRoot) {
542
- if (isArrayClass(data)) {
543
- return first(data, dataPath, dataRoot)
544
- && second(data, dataPath, dataRoot);
545
- }
546
- return true;
568
+ if (schemaObj.options.skipErrors) {
569
+ return function validateArraySchemaDouble(data, dataPath, dataRoot) {
570
+ if (isArrayClass(data)) {
571
+ return first(data, dataPath, dataRoot)
572
+ && second(data, dataPath, dataRoot);
573
+ }
574
+ return true;
575
+ };
576
+ }
577
+ // The two parts are independent array keyword groups (length/uniqueness
578
+ // versus the item walk); the array-class guard stays a precondition.
579
+ return function validateArraySchemaDoubleAll(data, dataPath, dataRoot) {
580
+ if (!isArrayClass(data)) return true;
581
+ const firstOk = first(data, dataPath, dataRoot);
582
+ return second(data, dataPath, dataRoot) && firstOk;
547
583
  };
548
584
  }
549
585
 
@@ -551,13 +587,22 @@ export function compileArraySchema(schemaObj, jsonSchema) {
551
587
  const hasBooleanItems = compiledItemsBoolean || trueThat;
552
588
  const hasBooleanContains = compiledContainsBoolean || trueThat;
553
589
  const validateItems = compiledArrayChildren || trueThat;
590
+ const stopAtFirstSchema = schemaObj.options.skipErrors;
554
591
 
555
592
  return function validateArraySchema(data, dataPath, dataRoot) {
556
593
  if (isArrayClass(data)) {
557
- return validatePrimitives(data, dataPath)
558
- && hasBooleanItems(data, dataPath, dataRoot)
559
- && hasBooleanContains(data, dataPath, dataRoot)
560
- && validateItems(data, dataPath, dataRoot);
594
+ if (stopAtFirstSchema) {
595
+ return validatePrimitives(data, dataPath)
596
+ && hasBooleanItems(data, dataPath, dataRoot)
597
+ && hasBooleanContains(data, dataPath, dataRoot)
598
+ && validateItems(data, dataPath, dataRoot);
599
+ }
600
+ // Length/uniqueness, the boolean items/contains forms and the item
601
+ // walk are independent; a length failure must not hide item faults.
602
+ let valid = validatePrimitives(data, dataPath);
603
+ valid = hasBooleanItems(data, dataPath, dataRoot) && valid;
604
+ valid = hasBooleanContains(data, dataPath, dataRoot) && valid;
605
+ return validateItems(data, dataPath, dataRoot) && valid;
561
606
  }
562
607
  return true;
563
608
  };
package/src/bigint.js CHANGED
@@ -86,12 +86,23 @@ export function compileBigIntBasic(schemaObj, jsonSchema) {
86
86
  const isMin = minimum || trueThat;
87
87
  const isMul = multipleOf || trueThat;
88
88
 
89
- return function validateBigIntSchema(data, dataPath) {
90
- if (isBigIntType(data)) {
91
- return isMax(data, dataPath)
92
- && isMin(data, dataPath)
93
- && isMul(data, dataPath);
94
- }
95
- return true;
89
+ if (schemaObj.options.skipErrors) {
90
+ return function validateBigIntSchema(data, dataPath) {
91
+ if (isBigIntType(data)) {
92
+ return isMax(data, dataPath)
93
+ && isMin(data, dataPath)
94
+ && isMul(data, dataPath);
95
+ }
96
+ return true;
97
+ };
98
+ }
99
+
100
+ // The type guard stays a precondition; the three assertions inside it are
101
+ // independent and each must get to report its own fault.
102
+ return function validateBigIntSchemaAll(data, dataPath) {
103
+ if (!isBigIntType(data)) return true;
104
+ let valid = isMax(data, dataPath);
105
+ valid = isMin(data, dataPath) && valid;
106
+ return isMul(data, dataPath) && valid;
96
107
  };
97
108
  }