diff --git a/packages/core/src/reducers/core.ts b/packages/core/src/reducers/core.ts index 550294250..c6cbab647 100644 --- a/packages/core/src/reducers/core.ts +++ b/packages/core/src/reducers/core.ts @@ -340,20 +340,47 @@ export const errorsAt = ( schema: JsonSchema, matchPath: (path: string) => boolean ) => (errors: ErrorObject[]): ErrorObject[] => { + // Get data paths of oneOf and anyOf errors to later determine whether an error occurred inside a subschema of oneOf or anyOf. const combinatorPaths = filter( errors, error => error.keyword === 'oneOf' || error.keyword === 'anyOf' ).map(error => error.dataPath); return filter(errors, error => { + // Filter errors that match any keyword that we don't want to show in the UI + if (filteredErrorKeywords.indexOf(error.keyword) !== -1) { + return false; + } + let result = matchPath(error.dataPath); - if (combinatorPaths.findIndex(p => instancePath.startsWith(p)) !== -1) { - result = result && isEqual(error.parentSchema, schema); + // In anyOf and oneOf blocks with "primitive subschemas" (not defining an object), + // we want to make sure that errors are only shown for the correct subschema. + // Therefore, we compare the error's parent schema with the property's schema. + // This is necessary because in the primitive case the error's data path is the same for all subschemas: + // It directly points to the property defining the anyOf/oneOf. + // In contrast, this comparison must not be done for errors whose parent schema defines an object + // because the parent schema can never match the property schema (e.g. for 'required' checks). + const parentSchema: JsonSchema | undefined = error.parentSchema; + if (result && parentSchema?.type !== 'object' + && combinatorPaths.findIndex(p => instancePath.startsWith(p)) !== -1) { + result = result && isEqual(parentSchema, schema); } return result; }); }; +/** + * The error-type of an AJV error is defined by its `keyword` property. + * Certain errors are filtered because they don't fit to any rendered control. + * All of them have in common that we don't want to show them in the UI + * because controls will show the actual reason why they don't match their correponding sub schema. + * - additionalProperties: Indicates that a property is present that is not defined in the schema. + * Jsonforms only allows to edit defined properties. These errors occur if an oneOf doesn't match. + * - anyOf: Indicates that an anyOf definition itself is not valid because none of its subschemas matches. + * - oneOf: Indicates that an oneOf definition itself is not valid because not exactly one of its subschemas matches. + */ +const filteredErrorKeywords = ['additionalProperties', 'anyOf', 'oneOf']; + const getErrorsAt = ( instancePath: string, schema: JsonSchema, diff --git a/packages/core/test/reducers/core.test.ts b/packages/core/test/reducers/core.test.ts index 3effe28cd..d55563b7d 100644 --- a/packages/core/test/reducers/core.test.ts +++ b/packages/core/test/reducers/core.test.ts @@ -754,6 +754,110 @@ test('errorAt filters required', t => { t.deepEqual(filtered[0], state.errors[1]); }); +test('errorAt filters required in oneOf object', t => { + const ajv = createAjv(); + const schema: JsonSchema = { + type: 'object', + properties: { + fooOrBar: { + oneOf: [ + { + title: 'Foo', + type: 'object', + properties: { + foo: { + type: 'string' + } + }, + required: ['foo'], + additionalProperties: false + }, + { + title: 'Bar', + type: 'object', + properties: { + bar: { + type: 'number' + } + }, + required: ['bar'], + additionalProperties: false + } + ] + } + }, + additionalProperties: false + }; + const data = { fooOrBar: { } }; + const v = ajv.compile(schema); + const errors = sanitizeErrors(v, data); + + const state: JsonFormsCore = { + data, + schema, + uischema: undefined, + errors + }; + const filtered = errorAt( + 'fooOrBar.foo', + schema.properties.fooOrBar.oneOf[0].properties.foo + )(state); + t.is(filtered.length, 1); + t.deepEqual(filtered[0].keyword, 'required'); +}); + +test('errorAt filters required in anyOf object', t => { + const ajv = createAjv(); + const schema: JsonSchema = { + type: 'object', + properties: { + fooOrBar: { + anyOf: [ + { + title: 'Foo', + type: 'object', + properties: { + foo: { + type: 'string' + } + }, + required: ['foo'], + additionalProperties: false + }, + { + title: 'Bar', + type: 'object', + properties: { + bar: { + type: 'number' + } + }, + required: ['bar'], + additionalProperties: false + } + ] + } + }, + additionalProperties: false + }; + const data = { fooOrBar: { } }; + const v = ajv.compile(schema); + const errors = sanitizeErrors(v, data); + + const state: JsonFormsCore = { + data, + schema, + uischema: undefined, + errors + }; + const filtered = errorAt( + 'fooOrBar.foo', + schema.properties.fooOrBar.anyOf[0].properties.foo + )(state); + t.is(filtered.length, 1); + t.deepEqual(filtered[0].keyword, 'required'); +}); + test('errorAt filters array minItems', t => { const ajv = createAjv(); const schema: JsonSchema = { @@ -973,6 +1077,7 @@ test('errorAt filters oneOf objects', t => { schema.properties.coloursOrNumbers.oneOf[1].properties.colour )(state); t.is(filtered.length, 1); + t.is(filtered[0].keyword, 'enum'); t.deepEqual(filtered[0], state.errors[1]); }); diff --git a/packages/examples/src/oneOf.ts b/packages/examples/src/oneOf.ts index d8bc04670..8d3a3ffda 100644 --- a/packages/examples/src/oneOf.ts +++ b/packages/examples/src/oneOf.ts @@ -35,7 +35,8 @@ export const schema = { city: { type: 'string' }, state: { type: 'string' } }, - required: ['street_address', 'city', 'state'] + required: ['street_address', 'city', 'state'], + additionalProperties: false }, user: { type: 'object', @@ -43,7 +44,8 @@ export const schema = { name: { type: 'string' }, mail: { type: 'string' } }, - required: ['name', 'mail'] + required: ['name', 'mail'], + additionalProperties: false } }, @@ -54,7 +56,8 @@ export const schema = { addressOrUser: { oneOf: [{ $ref: '#/definitions/address' }, { $ref: '#/definitions/user' }] } - } + }, + required: ['name'] }; export const uischema = {