From 2a8384b7e6366d71a5a74012b611d031bf9c08db Mon Sep 17 00:00:00 2001 From: Heath C <51679588+heath-freenome@users.noreply.github.com> Date: Thu, 22 Dec 2022 12:53:41 -0800 Subject: [PATCH] fix: 3260 by properly mapping missing required fields (#3309) fix #3260 The required field errors weren't mapped to the fields they were associated to in the `ErrorSchema` This mapping issue was fixed as follows: - In `@rjsf/core`, updated the `Form` and `validate` tests to fix the mapping issue in the test data for the required fields - In `@rjsf/validator-ajv6`, added additional tests for testing top-level and nested required field errors to verify things worked with ajv6 - In `@rjsf/validator-ajv8`, fixed the `transformRJSFValidationErrors()` function in the validator to look for `missingProperty: 'field'` information in the `params` property using that to properly map the error `property` - Replicated the tests made in `@rjsf/validator-ajv6` to this package to verify that the fix is working properly - Updated the `CHANGELOG.md` accordingly --- CHANGELOG.md | 1 + packages/core/test/Form_test.js | 4 +- packages/core/test/validate_test.js | 4 +- .../validator-ajv6/test/validator.test.ts | 65 ++++++ packages/validator-ajv8/src/validator.ts | 11 +- .../validator-ajv8/test/validator.test.ts | 201 ++++++++++++++++++ 6 files changed, 281 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f587615d1c..b594f05f06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ should change the heading of the (upcoming) version to include a major version b ## @rjsf/validator-ajv8 - Updated the validator to use the `ErrorSchemaBuilder` in the `toErrorSchema()` function to simplify the implementation +- Updated the validator to properly map missing required field errors in the `ErrorSchema`, fixing [#3260](https://github.com/rjsf-team/react-jsonschema-form/issues/3260) ## Dev / docs / playground - Fixed the documentation for `ArrayFieldItemTemplate` as part of the fix for [#3253](https://github.com/rjsf-team/react-jsonschema-form/issues/3253) diff --git a/packages/core/test/Form_test.js b/packages/core/test/Form_test.js index b852ebd296..4e0b8ce310 100644 --- a/packages/core/test/Form_test.js +++ b/packages/core/test/Form_test.js @@ -2220,7 +2220,9 @@ describeRepeated("Form common", (createFormComponent) => { field1: { __errors: ["must be number"], }, - __errors: ["must have required property 'field2'"], + field2: { + __errors: ["must have required property 'field2'"], + }, }, }, "root_field1" diff --git a/packages/core/test/validate_test.js b/packages/core/test/validate_test.js index 155bf0c407..7034fa5178 100644 --- a/packages/core/test/validate_test.js +++ b/packages/core/test/validate_test.js @@ -618,7 +618,7 @@ describe("Validation", () => { message: "must have required property 'foo'", name: "required", params: { missingProperty: "foo" }, - property: "", + property: "foo", schemaPath: "#/required", stack: "must have required property 'foo'", }, @@ -943,7 +943,7 @@ describe("Validation", () => { message: "must have required property 'foo'", name: "required", params: { missingProperty: "foo" }, - property: "", + property: "foo", schemaPath: "#/required", stack: "must have required property 'foo'", }, diff --git a/packages/validator-ajv6/test/validator.test.ts b/packages/validator-ajv6/test/validator.test.ts index 12e8727d55..38ca1e9da2 100644 --- a/packages/validator-ajv6/test/validator.test.ts +++ b/packages/validator-ajv6/test/validator.test.ts @@ -224,6 +224,71 @@ describe("AJV6Validator", () => { ]); }); }); + describe("Validating required fields", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + describe("formData is not provided at top level", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }; + + const formData = { pass1: "a" }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual(".pass2 is a required property"); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "is a required property" + ); + }); + }); + describe("formData is not provided for nested child", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + nested: { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }, + }, + }; + + const formData = { nested: { pass1: "a" } }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual( + ".nested.pass2 is a required property" + ); + }); + it("should return an errorSchema", () => { + expect(errorSchema.nested!.pass2!.__errors).toHaveLength(1); + expect(errorSchema.nested!.pass2!.__errors![0]).toEqual( + "is a required property" + ); + }); + }); + }); describe("No custom validate function, single additionalProperties value", () => { let errors: RJSFValidationError[]; let errorSchema: ErrorSchema; diff --git a/packages/validator-ajv8/src/validator.ts b/packages/validator-ajv8/src/validator.ts index 7802a20c67..d4038e3c12 100644 --- a/packages/validator-ajv8/src/validator.ts +++ b/packages/validator-ajv8/src/validator.ts @@ -203,7 +203,14 @@ export default class AJV8Validator< ): RJSFValidationError[] { return errors.map((e: ErrorObject) => { const { instancePath, keyword, message, params, schemaPath } = e; - const property = instancePath.replace(/\//g, "."); + let property = instancePath.replace(/\//g, "."); + let stack = `${property} ${message}`.trim(); + if ("missingProperty" in params) { + property = property + ? `${property}.${params.missingProperty}` + : params.missingProperty; + stack = message!; + } // put data in expected format return { @@ -211,7 +218,7 @@ export default class AJV8Validator< property, message, params, // specific to ajv - stack: `${property} ${message}`.trim(), + stack, schemaPath, }; }); diff --git a/packages/validator-ajv8/test/validator.test.ts b/packages/validator-ajv8/test/validator.test.ts index 8b9a0fb2a6..b90f3b4bb1 100644 --- a/packages/validator-ajv8/test/validator.test.ts +++ b/packages/validator-ajv8/test/validator.test.ts @@ -254,6 +254,73 @@ describe("AJV8Validator", () => { ]); }); }); + describe("Validating required fields", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + describe("formData is not provided at top level", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }; + + const formData = { pass1: "a" }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual( + "must have required property 'pass2'" + ); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "must have required property 'pass2'" + ); + }); + }); + describe("formData is not provided for nested child", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + nested: { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }, + }, + }; + + const formData = { nested: { pass1: "a" } }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual( + "must have required property 'pass2'" + ); + }); + it("should return an errorSchema", () => { + expect(errorSchema.nested!.pass2!.__errors).toHaveLength(1); + expect(errorSchema.nested!.pass2!.__errors![0]).toEqual( + "must have required property 'pass2'" + ); + }); + }); + }); describe("No custom validate function, single additionalProperties value", () => { let errors: RJSFValidationError[]; let errorSchema: ErrorSchema; @@ -695,6 +762,73 @@ describe("AJV8Validator", () => { ]); }); }); + describe("Validating required fields", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + describe("formData is not provided at top level", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }; + + const formData = { pass1: "a" }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual( + "must have required property 'pass2'" + ); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "must have required property 'pass2'" + ); + }); + }); + describe("formData is not provided for nested child", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + nested: { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }, + }, + }; + + const formData = { nested: { pass1: "a" } }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual( + "must have required property 'pass2'" + ); + }); + it("should return an errorSchema", () => { + expect(errorSchema.nested!.pass2!.__errors).toHaveLength(1); + expect(errorSchema.nested!.pass2!.__errors![0]).toEqual( + "must have required property 'pass2'" + ); + }); + }); + }); describe("No custom validate function, single additionalProperties value", () => { let errors: RJSFValidationError[]; let errorSchema: ErrorSchema; @@ -1137,6 +1271,73 @@ describe("AJV8Validator", () => { ]); }); }); + describe("Validating required fields", () => { + let errors: RJSFValidationError[]; + let errorSchema: ErrorSchema; + describe("formData is not provided at top level", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }; + + const formData = { pass1: "a" }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual( + "must have required property 'pass2'" + ); + }); + it("should return an errorSchema", () => { + expect(errorSchema.pass2!.__errors).toHaveLength(1); + expect(errorSchema.pass2!.__errors![0]).toEqual( + "must have required property 'pass2'" + ); + }); + }); + describe("formData is not provided for nested child", () => { + beforeAll(() => { + const schema: RJSFSchema = { + type: "object", + properties: { + nested: { + type: "object", + required: ["pass1", "pass2"], + properties: { + pass1: { type: "string" }, + pass2: { type: "string" }, + }, + }, + }, + }; + + const formData = { nested: { pass1: "a" } }; + const result = validator.validateFormData(formData, schema); + errors = result.errors; + errorSchema = result.errorSchema; + }); + it("should return an error list", () => { + expect(errors).toHaveLength(1); + expect(errors[0].stack).toEqual( + "must have required property 'pass2'" + ); + }); + it("should return an errorSchema", () => { + expect(errorSchema.nested!.pass2!.__errors).toHaveLength(1); + expect(errorSchema.nested!.pass2!.__errors![0]).toEqual( + "must have required property 'pass2'" + ); + }); + }); + }); describe("No custom validate function, single additionalProperties value", () => { let errors: RJSFValidationError[]; let errorSchema: ErrorSchema;