diff --git a/.changeset/hot-maps-marry.md b/.changeset/hot-maps-marry.md new file mode 100644 index 0000000..1deb99c --- /dev/null +++ b/.changeset/hot-maps-marry.md @@ -0,0 +1,5 @@ +--- +"codemirror-json-schema": patch +--- + +Fixed validation bugs: single objects incorrectly passed array schemas, invalid YAML caused errors after root-level change(now skipped if unparseable), and added tests ensuring non-array values(object, boolean, string, number) are correctly rejected. diff --git a/src/features/__tests__/__fixtures__/schemas.ts b/src/features/__tests__/__fixtures__/schemas.ts index 1a9d1f1..14fbc65 100644 --- a/src/features/__tests__/__fixtures__/schemas.ts +++ b/src/features/__tests__/__fixtures__/schemas.ts @@ -245,3 +245,10 @@ export const wrappedTestSchemaConditionalPropertiesOnSameObject = { }, required: ["original"], } as JSONSchema7; + +export const testSchemaArrayOfObjects = { + type: "array", + items: { + type: "object", + }, +} as JSONSchema7; diff --git a/src/features/__tests__/json-validation.spec.ts b/src/features/__tests__/json-validation.spec.ts index 3fcc536..1657795 100644 --- a/src/features/__tests__/json-validation.spec.ts +++ b/src/features/__tests__/json-validation.spec.ts @@ -4,7 +4,11 @@ import type { Diagnostic } from "@codemirror/lint"; import { describe, it, expect } from "vitest"; import { EditorView } from "@codemirror/view"; -import { testSchema, testSchema2 } from "./__fixtures__/schemas"; +import { + testSchema, + testSchema2, + testSchemaArrayOfObjects, +} from "./__fixtures__/schemas"; import { JSONMode } from "../../types"; import { getExtensions } from "./__helpers__/index"; import { MODES } from "../../constants"; @@ -12,7 +16,7 @@ import { MODES } from "../../constants"; const getErrors = ( jsonString: string, mode: JSONMode, - schema?: JSONSchema7 + schema?: JSONSchema7, ) => { const view = new EditorView({ doc: jsonString, @@ -30,13 +34,13 @@ const expectErrors = ( jsonString: string, errors: [from: number | undefined, to: number | undefined, message: string][], mode: JSONMode, - schema?: JSONSchema7 + schema?: JSONSchema7, ) => { const filteredErrors = getErrors(jsonString, mode, schema).map( - ({ renderMessage, ...error }) => error + ({ renderMessage, ...error }) => error, ); expect(filteredErrors).toEqual( - errors.map(([from, to, message]) => ({ ...common, from, to, message })) + errors.map(([from, to, message]) => ({ ...common, from, to, message })), ); }; @@ -145,6 +149,65 @@ describe("json-validation", () => { ], schema: testSchema2, }, + { + name: "reject a single object when schema expects an array", + mode: MODES.JSON, + doc: '{ "name": "John" }', + errors: [ + { + from: 0, + to: 0, + message: "Expected `array` but received `object`", + }, + ], + schema: testSchemaArrayOfObjects, + }, + { + name: "reject a boolean when schema expects an array", + mode: MODES.JSON, + doc: "true", + errors: [ + { + from: 0, + to: 0, + message: "Expected `array` but received `boolean`", + }, + ], + schema: testSchemaArrayOfObjects, + }, + { + name: "reject a string when schema expects an array", + mode: MODES.JSON, + doc: '"example"', + errors: [ + { + from: 0, + to: 0, + message: "Expected `array` but received `string`", + }, + ], + schema: testSchemaArrayOfObjects, + }, + { + name: "reject a number when schema expects an array", + mode: MODES.JSON, + doc: "123", + errors: [ + { + from: 0, + to: 0, + message: "Expected `array` but received `number`", + }, + ], + schema: testSchemaArrayOfObjects, + }, + { + name: "can handle an array of objects", + mode: MODES.JSON, + doc: '[{"name": "John"}, {"name": "Jane"}]', + errors: [], + schema: testSchemaArrayOfObjects, + }, ]; it.each([ ...jsonSuite, @@ -360,7 +423,7 @@ oneOfEg2: 123 doc, errors.map((error) => [error.from, error.to, error.message]), mode, - schema + schema, ); }); }); diff --git a/src/features/validation.ts b/src/features/validation.ts index 8fadbb6..3468f1d 100644 --- a/src/features/validation.ts +++ b/src/features/validation.ts @@ -85,7 +85,7 @@ export class JSONValidation { if (error.code === "one-of-error" && errors?.length) { return `Expected one of ${joinWithOr( errors, - (data) => data.data.expected + (data) => data.data.expected, )}`; } if (error.code === "type-error") { @@ -119,6 +119,8 @@ export class JSONValidation { if (!text?.length) return []; const json = this.parser(view.state); + // skip validation if parsing fails + if (json.data == null) return []; let errors: JsonError[] = []; try { @@ -147,7 +149,8 @@ export class JSONValidation { const pointer = json.pointers.get(errorPath) as JSONPointerData; if ( error.name === "MaxPropertiesError" || - error.name === "MinPropertiesError" + error.name === "MinPropertiesError" || + errorPath === "" // root level type errors ) { pushRoot(); } else if (pointer) {