ajv-validator / ajv

The fastest JSON schema Validator. Supports JSON Schema draft-04/06/07/2019-09/2020-12 and JSON Type Definition (RFC8927)
https://ajv.js.org
MIT License
13.87k stars 877 forks source link

contains with maxContains and minContains not behaving as expected #2461

Closed jamesbmorris92 closed 4 months ago

jamesbmorris92 commented 5 months ago

What version of Ajv are you using? Does the issue happen if you use the latest version? "version": "8.12.0",

Ajv options object

import AJV from "ajv/dist/2019";

    const ajv = new AJV({
        $data: true,
        useDefaults: true,
        allErrors: true,
        verbose: true,
        strict: false
    })

JSON Schema

        "testFields": {
                    "type": "array",
                    "minItems": 1,
                    "contains": {
                      "properties": {
                        "lastUpdatedAtTime": {
                          "const": true
                        }
                      }
                    },
                    "minContains": 1,
                    "maxContains": 1,
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "description": "",
                          "type": "string"
                        },
                        "lastUpdatedAtTime": {
                          "description": "Whether the field is a 'Date-time modified' field.",
                          "type": "boolean",
                          "default": false
                        }
                      },
                      "additionalProperties": false
                    },
                    "uniqueItemProperties": [
                      "name"
                    ]
                  }

Sample data

[
    {
        "lastUpdatedAtTime": true,
        "name": "test1"
    },
    {
        "lastUpdatedAtTime": false,
        "name": "test2"
    },
    {
        "lastUpdatedAtTime": false,
        "name": "test3"
    },
    {
        "lastUpdatedAtTime": true,
        "name": "test4"
    }
]

Your code

    const validate = ajv.compile(jsonSchema)
    validate(data);

Validation result, data AFTER validation, error messages

[
    {
        "instancePath": "/solutionFactoryConfiguration/viObjects/entities/1/testFields/1/lastUpdatedAtTime",
        "schemaPath": "#/properties/solutionFactoryConfiguration/properties/viObjects/properties/entities/items/properties/testFields/contains/properties/lastUpdatedAtTime/const",
        "keyword": "const",
        "params": {
            "allowedValue": true
        },
        "message": "must be equal to constant",
        "schema": true,
        "parentSchema": {
            "const": true
        },
        "data": false
    },
    {
        "instancePath": "/solutionFactoryConfiguration/viObjects/entities/1/testFields/2/lastUpdatedAtTime",
        "schemaPath": "#/properties/solutionFactoryConfiguration/properties/viObjects/properties/entities/items/properties/testFields/contains/properties/lastUpdatedAtTime/const",
        "keyword": "const",
        "params": {
            "allowedValue": true
        },
        "message": "must be equal to constant",
        "schema": true,
        "parentSchema": {
            "const": true
        },
        "data": false
    },
    {
        "instancePath": "/solutionFactoryConfiguration/viObjects/entities/1/testFields",
        "schemaPath": "#/properties/solutionFactoryConfiguration/properties/viObjects/properties/entities/items/properties/testFields/contains",
        "keyword": "contains",
        "params": {
            "minContains": 1,
            "maxContains": 1
        },
        "message": "must contain at least 1 and no more than 1 valid item(s)",
        "schema": {
            "properties": {
                "lastUpdatedAtTime": {
                    "const": true
                }
            }
        },
        "parentSchema": {
            "type": "array",
            "minItems": 1,
            "contains": {
                "properties": {
                    "lastUpdatedAtTime": {
                        "const": true
                    }
                }
            },
            "minContains": 1,
            "maxContains": 1,
            "items": {
                "type": "object",
                "properties": {
                    "name": {
                        "description": "",
                        "type": "string"
                    },
                    "lastUpdatedAtTime": {
                        "description": "Whether the field is a 'Date-time modified' field.",
                        "type": "boolean",
                        "default": false
                    }
                },
                "additionalProperties": false
            },
            "uniqueItemProperties": [
                "name"
            ]
        },
        "data": [
            {
                "name": "test1",
                "lastUpdatedAtTime": true
            },
            {
                "name": "test2",
                "lastUpdatedAtTime": false
            },
            {
                "name": "test3",
                "lastUpdatedAtTime": false
            },
            {
                "name": "test4",
                "lastUpdatedAtTime": true
            }
        ]
    }
]

What results did you expect? I got three errors where I expected just one. I expected the third error as a result of using contains with maxContains and minContains because I had more than one row of test data where lastUpdatedAtTime: true. However I didn't expect the first two errors that say the lastUpdatedAtTime must be equal to constant and the only permitted value is true.

Are you going to resolve the issue? I would like to but i'm not sure how. Apologies if i've made any mistakes or missed anything above this is my first issue submission.

jasoniangreen commented 4 months ago

Hi @jamesbmorris92, just to help me investigate, could you setup an example of this in runkit?

jamesbmorris92 commented 4 months ago

Hi @jasoniangreen here's an example: https://runkit.com/jamesbmorris92/6682b96af2a88c000808ae0e

jasoniangreen commented 4 months ago

Hi @jasoniangreen here's an example: https://runkit.com/jamesbmorris92/6682b96af2a88c000808ae0e

Thanks @jamesbmorris92, I will look into this, but in the meantime, I have a question. It's quite a complex / deeply nested example, do you think the example can be simplified further while still exhibiting the problem?

jamesbmorris92 commented 4 months ago

@jasoniangreen my apologies, i've made the example a bit more simple with less nesting: https://runkit.com/jamesbmorris92/6682b96af2a88c000808ae0e

jamesbmorris92 commented 4 months ago

hi @jasoniangreen were you able to recreate the issue?

jasoniangreen commented 4 months ago

I can see what you report, I've just got to dig more into the docs to understand what should be expected. There are a lot of cases in AJV and JSON Schema where you will legitimately get more errors than you expect, I'm just trying to understand if this is one of those situations.

jasoniangreen commented 4 months ago

Ah @jamesbmorris92 if you remove the allErrors: true option, it get's the expected result of just the contains error. Does this solve your issue? All errors true means that AJV will continue to check your data after encountering the first error, which seems to be the case here.

jamesbmorris92 commented 4 months ago

@jasoniangreen i had to trim down my schema and data quite a lot for this example and it's plausible that there would be lots of other errors in the data and I want to report those other errors to users.

The first two errors, the ones where the message is "must be equal to constant" don't seem right to me? the way I understood the contains in combination with the minContains was that atleast one (minItems) and at most one (maxItems) of the items in the array needs to satisfy the contains. In my example this is true, the first object in the array with "lastUpdatedAtTime": true satisfies the condition, all the rest of the objects in the array do not? Apologies if i'm misunderstanding?

jasoniangreen commented 4 months ago

So I think this is just a product of how contains in AJV works through an array looking for items to match contains schema. So with allErrors true it is going to report on all errors that it finds along the way and in the case of contains finding the errors against that subschema is how it finds matches for contains... I feel like I've written contains too much.

With larger and more complex schemas it's inevitable that allErrors is going to get more and more noisy.

jasoniangreen commented 4 months ago

So unless @epoberezkin disagrees I think this should be closed as this is not a behaviour we will consider changing.

jamesbmorris92 commented 4 months ago

@jasoniangreen thanks for all your help digging into this for me. Does that mean that what I want where it validates for only a single instance of lastUpdatedDttm: true is not possible? is there any other functionality to achieve this?