Skip to content

Commit

Permalink
Improve error filtering for anyOf and oneOf
Browse files Browse the repository at this point in the history
- Do not filter object level validations (e.g. required) in anyOf/oneOf.
- Filter anyOf/oneOf/additionalProperties errors
- Extend core tests
  • Loading branch information
lucas-koehler committed Mar 12, 2021
1 parent e16e508 commit 91c5794
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 5 deletions.
31 changes: 29 additions & 2 deletions packages/core/src/reducers/core.ts
Expand Up @@ -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,
Expand Down
105 changes: 105 additions & 0 deletions packages/core/test/reducers/core.test.ts
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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]);
});

Expand Down
9 changes: 6 additions & 3 deletions packages/examples/src/oneOf.ts
Expand Up @@ -35,15 +35,17 @@ export const schema = {
city: { type: 'string' },
state: { type: 'string' }
},
required: ['street_address', 'city', 'state']
required: ['street_address', 'city', 'state'],
additionalProperties: false
},
user: {
type: 'object',
properties: {
name: { type: 'string' },
mail: { type: 'string' }
},
required: ['name', 'mail']
required: ['name', 'mail'],
additionalProperties: false
}
},

Expand All @@ -54,7 +56,8 @@ export const schema = {
addressOrUser: {
oneOf: [{ $ref: '#/definitions/address' }, { $ref: '#/definitions/user' }]
}
}
},
required: ['name']
};

export const uischema = {
Expand Down

0 comments on commit 91c5794

Please sign in to comment.