AJV ignores 'required' rule for properties in nested, referenced object

29 Views Asked by At

I have a class that allows me to validate API responses against schemas using AJV. It includes the _definitionsHelper object that stores reusable blocks of schema:

import Ajv from 'ajv';

export default class SchemaUtilities {
    validateSchema = (schema, response) => {
        const ajv = new Ajv();
        const validate = ajv.addSchema(this._definitionsHelper).compile(schema);
        const valid = validate(response);
    
        if (!valid) {
            this._getSchemaError(validate.errors).then((schemaError) => {
                throw new Error(`Schema validation failed. ${schemaError}`);
            });
        } else {
            cy.log('Schema validated.');
        }
    };

    private _definitionsHelper = {
        $id: 'customDefinitions',
        definitions: {
            baseResponseProperties: {
                type: 'object',
                properties: {
                    errorCode: {
                        type: 'number'
                    },
                    responseCode: {
                        type: 'number'
                    },
                },
                required: ['errorCode', 'responseCode']
            },
            FOO: {
                type: 'object',
                properties: {
                    id: { type: 'number' },
                    definition: { type: 'number' },
                    url: { type: 'string' }
                },
                required: ['id', 'definition', 'url']
            },
            BAR: {
                type: 'object',
                properties: {
                    featureId: { type: 'number' },
                    order: { type: 'number' },
                    featureImageUrl: { type: 'string' },
                    navigationLinkType: { type: 'string' },
                    navigationLink: { type: ['string', 'null'] },
                    FOO: { type: ['object', 'null'], items: { $ref: 'customDefinitions#/definitions/FOO' } },
                    sku: { type: ['string', 'null'] },
                    yyyUrl: { type: ['string', 'null'] },
                    featureYYYAlignment: { type: ['string', 'null'] }
                },
                required: ['featureId', 'order', 'featureImageUrl', 'navigationLinkType', 'navigationLink', 'FOO', 'sku', 'yyyUrl', 'featureYYYAlignment']
            }
        },
    };

    private _getSchemaError = (getAjvError) => {
        return cy.wrap(`Field: ${getAjvError[0]['instancePath']} is invalid. Cause: ${getAjvError[0]['message'].replace(',', ', found ')}`);
    };
}

In BAR, I set FOO's type to ['object', 'null'] and removed one of the properties from my mock JSON response.

Mock JSON response (having removed the required 'url' property from FOO):

{
    "errorCode": 0,
    "responseCode": 0,
    "BAR": [{
        "featureId": 2,
        "order": 2,
        "featureImageUrl": "x",
        "navigationLinkType": "x",
        "navigationLink": null,
        "FOO": {
            "id": 2,
            "definition": 2
        },
        "sku": "x",
        "yyyUrl": null,
        "featureYYYAlignment": "x"
    }]
}

Expected result: an error should be thrown advising that the removed property is required.

Actual result: successfully validates.

Troubleshooting:

  1. Removed properties from the root of the mock JSON response to ensure AJV is correctly erroring when required properties are missing. It is.

  2. In _definitionsHelper, if I change FOO's type to 'array' or ['array', 'null'] and in the mock JSON response, I wrap FOO's object in an array, AJV throws the expected error. But this is of no use to me because the API returns FOO as an object, not an array with an object inside it.

The schema file that's validated against the mock JSON response:

export const featuresSchema = {
    type: 'object',
    $ref: 'customDefinitions#/definitions/baseResponseProperties',
    properties: {
        BAR: { type: 'array', items: { $ref: 'customDefinitions#/definitions/BAR' } }
    },
    required: ['BAR']
};

I've read Stackoverflow's suggested threads but couldn't find one that matches this issue.

Thank you for any help you can provide.

1

There are 1 best solutions below

0
Jeremy Fiel On

Depends on the JSON Schema version used which you didn't indicate, AJV defaults to draft-07 with the import statement you are using

Couple things:

  • Your schema definition is partially ignored because you can't have siblings to a $ref in older draft versions of JSON Schema. So the $ref defined with baseResponseProperties is validated but the BAR schema is ignored, thus why you're not returning the expected error.

Try this.

export const featuresSchema = {
    allOf: [
        { $ref: 'customDefinitions#/definitions/baseResponseProperties' },
        {
            type: 'object',
            properties: {
                BAR: {
                    type: 'array', items: { $ref: 'customDefinitions#/definitions/BAR' }
                }
            },
            required: ['BAR']
        }
    ]
};

The second issue is FOO is only defined with type: ['object', 'null'] and you have an items keyword defined. These items will never be evaluated because you are constraining the schema to only the two defined types.

#invalid
FOO: { type: ['object', 'null'], items: { $ref: customDefinitions#/definitions/FOO' } },
#valid
// add array type
FOO: { type: ['object', 'null', 'array'], items: { $ref: customDefinitions#/definitions/FOO' } },

OR
// as an object
FOO: { type: ['object', 'null'], $ref: customDefinitions#/definitions/FOO' },