ajv-validator / ajv-formats

JSON Schema format validation for Ajv v8+
https://ajv.js.org
MIT License
183 stars 36 forks source link

“compare is not a function” when using formatMinimum with date format #56

Open brandondurham opened 2 years ago

brandondurham commented 2 years ago

This is my schema (with unrelated properties removed):

export const employeeSchema = {
    items: {
        properties: {
            launchDate: {
                errorMessage: {
                    format: 'must be a date in YYYY-MM-DD format (e.g. _1973-05-11_)',
                    type: 'required, and must be a date in YYYY-MM-DD format (e.g. _1973-05-11_)',
                },
                examples: ['1973-05-11'],
                format: 'date',
                type: 'string',
            },
        },
        required: [
            'launchDate',
        ],
        type: 'object',
    },
    minItems: 1,
    type: 'array',
};

As-is, everything works and validates correctly. But when I add formatMinimum to launchDate:

export const employeeSchema = {
    items: {
        properties: {
            launchDate: {
                errorMessage: {
                    format: 'must be a date in YYYY-MM-DD format (e.g. _1973-05-11_)',
                    type: 'required, and must be a date in YYYY-MM-DD format (e.g. _1973-05-11_)',
                },
                examples: ['1973-05-11'],
                format: 'date',
                formatMinimum: '2022-03-30',
                type: 'string',
            },
        },
        required: [
            'launchDate',
        ],
        type: 'object',
    },
    minItems: 1,
    type: 'array',
};

… I get the following console error:

Uncaught TypeError: {(intermediate value)(intermediate value)(intermediate value)(intermediate value)}.compare is not a function
    at validate14 (eval at compileSchema (index.jss:89:1), <anonymous>:3:2858)
    at index.jss:149:1
    at mountMemo (react-dom.development.jss:15846:1)
    at Object.useMemo (react-dom.development.jss:16219:1)
    at useMemo (react.development.jss:1532:1)
    at UseMemo (whyDidYouRender.jss:1461:1)
    at ModalImportReview (index.jss:132:1)
    at renderWithHooks (react-dom.development.jss:14985:1)
    at mountIndeterminateComponent (react-dom.development.jss:17811:1)
    at beginWork (react-dom.development.jss:19049:1)

Update:

RunKit example: https://runkit.com/brandondurham/compare-is-not-a-function-when-using-formatminimum-with-date-format

epoberezkin commented 2 years ago

Please add a minimal code sample - ideally to runkit - it's not clear what you are doing wrong otherwise...

Most likely you are not passing the correct options to ajv-formats.

brandondurham commented 2 years ago

Added a RunKit example above! Thanks so much for looking.

cdxOo commented 2 years ago

any updates on that one? it also occurs when using the example in your documentation, which i basically copied verbatim into runkit your docs: https://github.com/ajv-validator/ajv-formats#keywords-to-compare-values-formatmaximum--formatminimum-and-formatexclusivemaximum--formatexclusiveminimum

runkit A: https://runkit.com/cdxoo/62f79994b6b4b90009190fe4 for interaction with ajv@8.11.0 runkit B: https://runkit.com/cdxoo/62f7953878d623000706463a for interaction with ajv@7.2.4

the issue occurs in both of those

var Ajv = require('ajv@8.11.0');
var ajvFormats = require('ajv-formats@2.1.1');

var ajv = new Ajv();
ajvFormats(ajv);

var schema = {
    type: "string",
    format: "date",
    formatMinimum: "2016-02-06",
    formatExclusiveMaximum: "2016-12-27",
}

ajv.validate(schema, '2016-02-06');
ricardogoncalves89 commented 1 year ago

I think the problem comes after ajv-formats@2.0.1. Can someone confirm?

taylorreece commented 1 year ago

It does appear that using ajv-formats@2.0.1 works fine - ajv-formats@2.0.2 does not

jameswinglmi commented 7 months ago

Here is an example of the (faulty) validation code being generated by the library for a schema with type=string, format=date-time or format=date, and formatMinimum specified (presumably it does the same for formatMaximum too):

(function anonymous(self, scope) {
    const schema16 = scope.schema[10];
    const formats0 = scope.formats[0];
    return function validate14(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}) {
        let vErrors = null;
        let errors = 0;
        if (typeof data === "string") {
            if (!(formats0.validate(data))) {
                const err0 = {
                    instancePath,
                    schemaPath: "#/format",
                    keyword: "format",
                    params: {
                        format: "date-time"
                    },
                    message: "must match format \"" + "date-time" + "\""
                };
                if (vErrors === null) {
                    vErrors = [err0];
                } else {
                    vErrors.push(err0);
                }
                errors++;
            }
            if ({
                "str": "formats0",
                "prefix": "formats",
                "value": {
                    "key": "date-time",
                    "ref": {},
                    "code": {
                        "_items": ["", "{\"_items\":[\"require(\\\"ajv-formats/dist/formats\\\").\",{\"str\":\"fullFormats\"},\"\"]}", "", "[", "\"date-time\"", "]", ""]
                    }
                },
                "scopePath": {
                    "_items": [".", {
                        "str": "formats"
                    }, "[", 0, "]"]
                }
            }.compare({  // <<<<<<======== this throws because the above ~14 lines didn't get turned into code (and the ~3 below)
                "str": "data"
            }, {
                "_items": ["", "\"1970-01-01T00:00:00.000Z\"", ""]
            }) < 0) {
                const err1 = {
                    instancePath,
                    schemaPath: "#/formatMinimum",
                    keyword: "formatMinimum",
                    params: {
                        "_items": ["{comparison: ", "\">=\"", ", limit: ", "{\"_items\":[\"\",\"\\\"1970-01-01T00:00:00.000Z\\\"\",\"\"]}", "}"]
                    },
                    message: {
                        "_items": ["\"should be >= \"", "+", "{\"_items\":[\"\",\"\\\"1970-01-01T00:00:00.000Z\\\"\",\"\"]}"]
                    }
                };
                if (vErrors === null) {
                    vErrors = [err1];
                } else {
                    vErrors.push(err1);
                }
                errors++;
            }
        } else {
            const err2 = {
                instancePath,
                schemaPath: "#/type",
                keyword: "type",
                params: {
                    type: "string"
                },
                message: "must be string"
            };
            if (vErrors === null) {
                vErrors = [err2];
            } else {
                vErrors.push(err2);
            }
            errors++;
        }
        if (errors > 0) {
            const emErrs0 = [];
            for (const err3 of vErrors) {
                if (((((err3.keyword !== "errorMessage") && (!err3.emUsed)) && ((err3.instancePath === instancePath) || ((err3.instancePath.indexOf(instancePath) === 0) && (err3.instancePath[instancePath.length] === "/")))) && (err3.schemaPath.indexOf("#") === 0)) && (err3.schemaPath["#".length] === "/")) {
                    emErrs0.push(err3);
                    err3.emUsed = true;
                }
            }
            if (emErrs0.length) {
                const err4 = {
                    instancePath,
                    schemaPath: "#/errorMessage",
                    keyword: "errorMessage",
                    params: {
                        errors: emErrs0
                    },
                    message: "must be an ISO-8601-formatted datetime (min 1970-01-01T00:00:00.000Z)"
                };
                if (vErrors === null) {
                    vErrors = [err4];
                } else {
                    vErrors.push(err4);
                }
                errors++;
            }
            const emErrs1 = [];
            for (const err5 of vErrors) {
                if (!err5.emUsed) {
                    emErrs1.push(err5);
                }
            }
            vErrors = emErrs1;
            errors = emErrs1.length;
        }
        validate14.errors = vErrors;
        return errors === 0;
    }
}
)

We can see that the code generation is failing to create valid javascript. It is leaving objects as placeholders for what it wants on either side of the .compare(.

I studied it for hours, but there is a steep learning curve to this library. The magic is being performed in the CodeGen class in node_modules\ajv\lib\compile\codegen\index.ts, upon being called by topSchemaObjCode() from within validateFunctionCode() in node_modules\ajv\lib\compile\validate\index.ts.

It was a little tricky to find, as it happens when the schema gets compiled for the first time.

I can surmise that the following block of generated validation javascript:

{
                "str": "formats0",
                "prefix": "formats",
                "value": {
                    "key": "date-time",
                    "ref": {},
                    "code": {
                        "_items": ["", "{\"_items\":[\"require(\\\"ajv-formats/dist/formats\\\").\",{\"str\":\"fullFormats\"},\"\"]}", "", "[", "\"date-time\"", "]", ""]
                    }
                },
                "scopePath": {
                    "_items": [".", {
                        "str": "formats"
                    }, "[", 0, "]"]
                }
            }.compare({
                "str": "data"
            }, {
                "_items": ["", "\"1970-01-01T00:00:00.000Z\"", ""]
            }

should instead be something like

formats0.compare("1970-01-01T00:00:00.000Z")

... and it simply didn't get turned into javascript correctly.

Hopefully this will help get if fixed more easily & quicker. In the meantime, I'd love to hear form the authoritative source what is the proposed workaround.

jameswinglmi commented 7 months ago

The workaround I used for now was to set the version explicitly in package.json dependencies ("ajv-formats": "2.0.1"), and then delete node_modules folder and the package-lock.json file, and recreate them with npm install