From 61248892bcd3679ea9612aaabb7d5fdb9fd00231 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Fri, 1 May 2026 17:15:37 -0700 Subject: [PATCH 1/2] Add in support for array keyword errors --- src/helpers.js | 5 + .../array/max-items/in-object/data.json | 3 + .../array/max-items/in-object/schema.json | 13 +++ .../array/max-items/root/data.json | 1 + .../array/max-items/root/schema.json | 5 + .../array/min-items/in-object/data.json | 3 + .../array/min-items/in-object/schema.json | 13 +++ .../array/min-items/root/data.json | 1 + .../array/min-items/root/schema.json | 5 + .../array/unique/in-object/data.json | 1 + .../array/unique/in-object/schema.json | 10 ++ .../__fixtures__/array/unique/root/data.json | 1 + .../array/unique/root/schema.json | 5 + .../__tests__/__snapshots__/array.js.snap | 109 ++++++++++++++++++ .../__tests__/__snapshots__/main.js.snap | 57 +++++++++ src/validation-errors/__tests__/array.js | 43 +++++++ src/validation-errors/__tests__/main.js | 19 +++ src/validation-errors/array.js | 27 +++++ src/validation-errors/index.js | 1 + 19 files changed, 322 insertions(+) create mode 100644 src/validation-errors/__fixtures__/array/max-items/in-object/data.json create mode 100644 src/validation-errors/__fixtures__/array/max-items/in-object/schema.json create mode 100644 src/validation-errors/__fixtures__/array/max-items/root/data.json create mode 100644 src/validation-errors/__fixtures__/array/max-items/root/schema.json create mode 100644 src/validation-errors/__fixtures__/array/min-items/in-object/data.json create mode 100644 src/validation-errors/__fixtures__/array/min-items/in-object/schema.json create mode 100644 src/validation-errors/__fixtures__/array/min-items/root/data.json create mode 100644 src/validation-errors/__fixtures__/array/min-items/root/schema.json create mode 100644 src/validation-errors/__fixtures__/array/unique/in-object/data.json create mode 100644 src/validation-errors/__fixtures__/array/unique/in-object/schema.json create mode 100644 src/validation-errors/__fixtures__/array/unique/root/data.json create mode 100644 src/validation-errors/__fixtures__/array/unique/root/schema.json create mode 100644 src/validation-errors/__tests__/__snapshots__/array.js.snap create mode 100644 src/validation-errors/__tests__/array.js create mode 100644 src/validation-errors/array.js diff --git a/src/helpers.js b/src/helpers.js index 13e30fad..d826b320 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -10,6 +10,7 @@ import { } from './utils'; import { AdditionalPropValidationError, + ArrayPropertyValidationError, RequiredValidationError, EnumValidationError, DefaultValidationError, @@ -142,6 +143,10 @@ export function createErrorInstances(root, options) { ); case 'required': return ret.concat(new RequiredValidationError(error, options)); + case 'maxItems': + case 'minItems': + case 'uniqueItems': + return ret.concat(new ArrayPropertyValidationError(error, options)); default: return ret.concat(new DefaultValidationError(error, options)); } diff --git a/src/validation-errors/__fixtures__/array/max-items/in-object/data.json b/src/validation-errors/__fixtures__/array/max-items/in-object/data.json new file mode 100644 index 00000000..95364c64 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/max-items/in-object/data.json @@ -0,0 +1,3 @@ +{ + "foo": [1, 2, 3] +} diff --git a/src/validation-errors/__fixtures__/array/max-items/in-object/schema.json b/src/validation-errors/__fixtures__/array/max-items/in-object/schema.json new file mode 100644 index 00000000..5d8a1a53 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/max-items/in-object/schema.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "foo": { + "type": "array", + "maxItems": 2, + "items": { + "type": "number" + } + } + } +} + diff --git a/src/validation-errors/__fixtures__/array/max-items/root/data.json b/src/validation-errors/__fixtures__/array/max-items/root/data.json new file mode 100644 index 00000000..b5d8bb58 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/max-items/root/data.json @@ -0,0 +1 @@ +[1, 2, 3] diff --git a/src/validation-errors/__fixtures__/array/max-items/root/schema.json b/src/validation-errors/__fixtures__/array/max-items/root/schema.json new file mode 100644 index 00000000..ef8d4197 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/max-items/root/schema.json @@ -0,0 +1,5 @@ +{ + "type": "array", + "maxItems": 2, + "items": { "type": "number" } +} diff --git a/src/validation-errors/__fixtures__/array/min-items/in-object/data.json b/src/validation-errors/__fixtures__/array/min-items/in-object/data.json new file mode 100644 index 00000000..95364c64 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/min-items/in-object/data.json @@ -0,0 +1,3 @@ +{ + "foo": [1, 2, 3] +} diff --git a/src/validation-errors/__fixtures__/array/min-items/in-object/schema.json b/src/validation-errors/__fixtures__/array/min-items/in-object/schema.json new file mode 100644 index 00000000..830f1347 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/min-items/in-object/schema.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "foo": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } +} + diff --git a/src/validation-errors/__fixtures__/array/min-items/root/data.json b/src/validation-errors/__fixtures__/array/min-items/root/data.json new file mode 100644 index 00000000..b5d8bb58 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/min-items/root/data.json @@ -0,0 +1 @@ +[1, 2, 3] diff --git a/src/validation-errors/__fixtures__/array/min-items/root/schema.json b/src/validation-errors/__fixtures__/array/min-items/root/schema.json new file mode 100644 index 00000000..05675c43 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/min-items/root/schema.json @@ -0,0 +1,5 @@ +{ + "type": "array", + "items": { "type": "number" }, + "minItems": 4 +} diff --git a/src/validation-errors/__fixtures__/array/unique/in-object/data.json b/src/validation-errors/__fixtures__/array/unique/in-object/data.json new file mode 100644 index 00000000..795573c4 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/unique/in-object/data.json @@ -0,0 +1 @@ +{ "foo": [1, 1, 3] } diff --git a/src/validation-errors/__fixtures__/array/unique/in-object/schema.json b/src/validation-errors/__fixtures__/array/unique/in-object/schema.json new file mode 100644 index 00000000..c11f77f8 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/unique/in-object/schema.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "foo": { + "type": "array", + "uniqueItems": true, + "items": { "type": "number" } + } + } +} diff --git a/src/validation-errors/__fixtures__/array/unique/root/data.json b/src/validation-errors/__fixtures__/array/unique/root/data.json new file mode 100644 index 00000000..99e93016 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/unique/root/data.json @@ -0,0 +1 @@ +[1, 1, 3] diff --git a/src/validation-errors/__fixtures__/array/unique/root/schema.json b/src/validation-errors/__fixtures__/array/unique/root/schema.json new file mode 100644 index 00000000..56eb2fc1 --- /dev/null +++ b/src/validation-errors/__fixtures__/array/unique/root/schema.json @@ -0,0 +1,5 @@ +{ + "type": "array", + "uniqueItems": true, + "items": { "type": "number" } +} diff --git a/src/validation-errors/__tests__/__snapshots__/array.js.snap b/src/validation-errors/__tests__/__snapshots__/array.js.snap new file mode 100644 index 00000000..904b6f1f --- /dev/null +++ b/src/validation-errors/__tests__/__snapshots__/array.js.snap @@ -0,0 +1,109 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Array Properties > 'Maximum Items' > for 'at root' > prints correctly, without confusing leading keyword 1`] = ` +[ + "/ must NOT have more than 2 items +", + "> 1 | [ +  | ^ +> 2 | 1, +  | ^^^^ +> 3 | 2, +  | ^^^^ +> 4 | 3 +  | ^^^^ +> 5 | ] +  | ^^ 👈🏽 array must NOT have more than 2 items", +] +`; + +exports[`Array Properties > 'Maximum Items' > for 'inside object' > prints correctly, without confusing leading keyword 1`] = ` +[ + "/foo must NOT have more than 2 items +", + "  1 | { +> 2 | "foo": [ +  | ^ +> 3 | 1, +  | ^^^^^^ +> 4 | 2, +  | ^^^^^^ +> 5 | 3 +  | ^^^^^^ +> 6 | ] +  | ^^^^ 👈🏽 array must NOT have more than 2 items +  7 | }", +] +`; + +exports[`Array Properties > 'Minimum Items' > for 'at root' > prints correctly, without confusing leading keyword 1`] = ` +[ + "/ must NOT have fewer than 4 items +", + "> 1 | [ +  | ^ +> 2 | 1, +  | ^^^^ +> 3 | 2, +  | ^^^^ +> 4 | 3 +  | ^^^^ +> 5 | ] +  | ^^ 👈🏽 array must NOT have fewer than 4 items", +] +`; + +exports[`Array Properties > 'Minimum Items' > for 'inside object' > prints correctly, without confusing leading keyword 1`] = ` +[ + "/foo must NOT have fewer than 4 items +", + "  1 | { +> 2 | "foo": [ +  | ^ +> 3 | 1, +  | ^^^^^^ +> 4 | 2, +  | ^^^^^^ +> 5 | 3 +  | ^^^^^^ +> 6 | ] +  | ^^^^ 👈🏽 array must NOT have fewer than 4 items +  7 | }", +] +`; + +exports[`Array Properties > 'Unique Items' > for 'at root' > prints correctly, without confusing leading keyword 1`] = ` +[ + "/ must NOT have duplicate items (items ## 1 and 0 are identical) +", + "> 1 | [ +  | ^ +> 2 | 1, +  | ^^^^ +> 3 | 1, +  | ^^^^ +> 4 | 3 +  | ^^^^ +> 5 | ] +  | ^^ 👈🏽 array must NOT have duplicate items (items ## 1 and 0 are identical)", +] +`; + +exports[`Array Properties > 'Unique Items' > for 'inside object' > prints correctly, without confusing leading keyword 1`] = ` +[ + "/foo must NOT have duplicate items (items ## 1 and 0 are identical) +", + "  1 | { +> 2 | "foo": [ +  | ^ +> 3 | 1, +  | ^^^^^^ +> 4 | 1, +  | ^^^^^^ +> 5 | 3 +  | ^^^^^^ +> 6 | ] +  | ^^^^ 👈🏽 array must NOT have duplicate items (items ## 1 and 0 are identical) +  7 | }", +] +`; diff --git a/src/validation-errors/__tests__/__snapshots__/main.js.snap b/src/validation-errors/__tests__/__snapshots__/main.js.snap index 5458ad74..0a83c113 100644 --- a/src/validation-errors/__tests__/__snapshots__/main.js.snap +++ b/src/validation-errors/__tests__/__snapshots__/main.js.snap @@ -1,5 +1,62 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Main > should support js output format for 'maxItems' errors 1`] = ` +[ + { + "end": { + "column": 8, + "line": 1, + "offset": 7, + }, + "error": "/: Array must NOT have more than 2 items", + "path": "", + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, +] +`; + +exports[`Main > should support js output format for 'minItems' errors 1`] = ` +[ + { + "end": { + "column": 8, + "line": 1, + "offset": 7, + }, + "error": "/: Array must NOT have fewer than 4 items", + "path": "", + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, +] +`; + +exports[`Main > should support js output format for 'uniqueItem' errors 1`] = ` +[ + { + "end": { + "column": 8, + "line": 1, + "offset": 7, + }, + "error": "/: Array must NOT have duplicate items (items ## 1 and 0 are identical)", + "path": "", + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, +] +`; + exports[`Main > should support js output format for additionalProperties errors 1`] = ` [ { diff --git a/src/validation-errors/__tests__/array.js b/src/validation-errors/__tests__/array.js new file mode 100644 index 00000000..0ac03020 --- /dev/null +++ b/src/validation-errors/__tests__/array.js @@ -0,0 +1,43 @@ +import Ajv from 'ajv'; +import { describe, it, expect, beforeEach } from 'vitest'; +const { parse } = require('@humanwhocodes/momoa'); + +import { getSchemaAndData } from '../../test-helpers'; +import { ArrayPropertyValidationError } from '../array'; + +describe('Array Properties', () => { + const ajv = new Ajv(); + let validator; + + describe.each([ + ['Minimum Items', 'min-items'], + ['Maximum Items', 'max-items'], + ['Unique Items', 'unique'], + ])('$0', (_title, directory) => { + describe.each([ + ['at root', 'root'], + ['inside object', 'in-object'], + ])('for $0', (_title, name) => { + let schema, data, jsonRaw, jsonAst; + + beforeEach(async () => { + [schema, data] = await getSchemaAndData(`array/${directory}/${name}`, __dirname); + jsonRaw = JSON.stringify(data, null, 2); + jsonAst = parse(jsonRaw); + validator = ajv.compile(schema) + }); + + it('prints correctly, without confusing leading keyword', async () => { + const valid = validator(data); + const error = new ArrayPropertyValidationError( + validator.errors[0], + { data, schema, jsonRaw, jsonAst }, + ); + + expect(valid).toBe(false); + expect(error.print()).toMatchSnapshot(); + }); + }); + }); +}); + diff --git a/src/validation-errors/__tests__/main.js b/src/validation-errors/__tests__/main.js index 838ca03f..7c9d0b2b 100644 --- a/src/validation-errors/__tests__/main.js +++ b/src/validation-errors/__tests__/main.js @@ -62,4 +62,23 @@ describe('Main', () => { }); expect(res).toMatchSnapshot(); }); + + it.each([ + ['maxItems', 'max-items'], + ['minItems', 'min-items'], + ['uniqueItem', 'unique'], + ])('should support js output format for $0 errors', async (_title, directory) => { + const [schema, data] = await getSchemaAndData(`array/${directory}/root`, __dirname); + + const ajv = new Ajv(); + const validate = ajv.compile(schema); + const valid = validate(data); + expect(valid).toBeFalsy(); + + const res = betterAjvErrors(schema, data, validate.errors, { + format: 'js', + }); + + expect(res).toMatchSnapshot(); + }); }); diff --git a/src/validation-errors/array.js b/src/validation-errors/array.js new file mode 100644 index 00000000..dfc1d3dc --- /dev/null +++ b/src/validation-errors/array.js @@ -0,0 +1,27 @@ +import chalk from 'chalk'; +import BaseValidationError from './base'; + +export class ArrayPropertyValidationError extends BaseValidationError { + print() { + const { message } = this.options; + const displayPath = this.instancePath.length === 0 ? '/' : this.instancePath; + + const output = [chalk`{red {bold ${displayPath}} ${message}}\n`]; + + return output.concat( + this.getCodeFrame(chalk`👈🏽 {magentaBright array} ${message}`) + ); + } + + getError() { + const { message } = this.options; + const decoratedPath = this.getDecoratedPath(); + const displayPath = decoratedPath.length === 0 ? '/' : decoratedPath; + + return { + ...this.getLocation(), + error: `${displayPath}: Array ${message}`, + path: this.instancePath, + }; + } +} diff --git a/src/validation-errors/index.js b/src/validation-errors/index.js index 39ff94b8..e7ab55d8 100644 --- a/src/validation-errors/index.js +++ b/src/validation-errors/index.js @@ -2,3 +2,4 @@ export { default as RequiredValidationError } from './required'; export { default as AdditionalPropValidationError } from './additional-prop'; export { default as EnumValidationError } from './enum'; export { default as DefaultValidationError } from './default'; +export { ArrayPropertyValidationError } from './array'; From 804a81f4b6af189396c445f84945f659ecad0b19 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Fri, 1 May 2026 17:17:59 -0700 Subject: [PATCH 2/2] Add changeset --- .changeset/brave-places-joke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/brave-places-joke.md diff --git a/.changeset/brave-places-joke.md b/.changeset/brave-places-joke.md new file mode 100644 index 00000000..ccb9f9f7 --- /dev/null +++ b/.changeset/brave-places-joke.md @@ -0,0 +1,5 @@ +--- +'better-ajv-errors': minor +--- + +Adds in support for array keyword errors maxItems, minItems, and uniqueItems. Some old errors that would have printed using the default error class will now display specialized array errors.