ajv-validator / ajv-errors

Custom error messages in JSON Schemas for Ajv validator
https://ajv.js.org
MIT License
282 stars 18 forks source link

`JSONSchemaType` + `anyOf` corresponding to union over objects cannot correctly determine missing fields #144

Open alexandervandekleutab opened 2 years ago

alexandervandekleutab commented 2 years ago

Problem description

AJV cannot infer which subtype of a union is being used when creating a union over objects. This might be an issue with AJV core, or just an issue with the error handling.

Below is a simple but complete example of the issue:

import ajvErrors from 'ajv-errors'
import addFormats from 'ajv-formats'
import Ajv, { JSONSchemaType } from 'ajv'

type UploadAction = {
  actionType: 'UPLOAD'
  url: string
  filename: string
}

type EmailAction = {
  actionType: 'EMAIL'
  email: string
}

type Action = UploadAction | EmailAction
type Actions = Array<Action>

const schema: JSONSchemaType<Actions> = {
  type: 'array',
  items: {
    anyOf: [
      {
        type: 'object',
        required: ['actionType', 'url', 'filename'],
        properties: {
          actionType: {
            type: 'string',
            const: 'UPLOAD',
          },
          url: {
            type: 'string',
          },
          filename: {
            type: 'string',
          },
        },
      },
      {
        type: 'object',
        required: ['actionType', 'email'],
        properties: {
          actionType: {
            type: 'string',
            const: 'EMAIL',
          },
          email: {
            type: 'string',
          },
        },
      },
    ],
  },
}

if (require.main === module) {
  const ajv = new Ajv({ allErrors: true })
  ajvErrors(ajv)
  addFormats(ajv)
  const validate = ajv.compile(schema)

  const data: Array<unknown> = [
    {
      actionType: 'UPLOAD',
    },
  ]

  if (validate(data)) {
    console.log(data)
  } else {
    console.log(validate.errors)
  }
}

Run this using ts-node or compile and run it via node. The output I get is

[
  {
    instancePath: '/0',
    schemaPath: '#/items/anyOf/0/required',
    keyword: 'required',
    params: { missingProperty: 'url' },
    message: "must have required property 'url'"
  },
  {
    instancePath: '/0',
    schemaPath: '#/items/anyOf/0/required',
    keyword: 'required',
    params: { missingProperty: 'filename' },
    message: "must have required property 'filename'"
  },
  {
    instancePath: '/0',
    schemaPath: '#/items/anyOf/1/required',
    keyword: 'required',
    params: { missingProperty: 'email' },
    message: "must have required property 'email'"
  },
  {
    instancePath: '/0/actionType',
    schemaPath: '#/items/anyOf/1/properties/actionType/const',
    keyword: 'const',
    params: { allowedValue: 'EMAIL' },
    message: 'must be equal to constant'
  },
  {
    instancePath: '/0',
    schemaPath: '#/items/anyOf',
    keyword: 'anyOf',
    params: {},
    message: 'must match a schema in anyOf'
  }
]

When I specify actionType: 'UPLOAD', I expect that the only missing fields should url and filename. The errors output by AJV are confusing and misleading.

In contrast, the typescript type inference engine is correctly able to deduce the the missing fields once the actionType field is set to one of EMAIL or UPLOAD.

Here are my versions via yarn list:

├─ ajv-errors@3.0.0
├─ ajv-formats@2.1.1
│  └─ ajv@^8.0.0
├─ ajv@8.8.2
│  ├─ fast-deep-equal@^3.1.1
│  ├─ json-schema-traverse@^1.0.0
│  ├─ require-from-string@^2.0.2
│  └─ uri-js@^4.2.2
dontbesatisfied commented 2 years ago

Does anyone know the solution?

18yy commented 1 month ago

How did you solve it?