jsonschemajson-schema-validatorajv

How do I validate data with differing nested $schema values with ajv?


I am trying to validate data with different nested $schema values against a schema that allows it, but it doesn't correctly validate the nested objects with the correct sub schema.

Here's a simplified example:

const ajv = new Ajv();

const mainSchema = {
  $id: "mainSchema",
  type: "object",
  properties: {
    foo: {
      anyOf: [{ $ref: "intSchema" }, { $ref: "strSchema" }],
    },
  },
};
const intSchema = {
  $id: "intSchema",
  type: "object",
  properties: {
    value: {
      type: "integer",
    },
  },
};
const strSchema = {
  $id: "strSchema",
  type: "object",
  properties: {
    value: {
      type: "string",
    },
  },
};

const dataA = {
  $schema: "mainSchema",
  foo: {
    $schema: "intSchema",
    value: 42,
  },
};

const dataB = {
  $schema: "mainSchema",
  foo: {
    $schema: "strSchema",
    value: 1,
  },
};

const dataC = {
  $schema: "mainSchema",
  foo: {
    $schema: "unknownSchema",
    value: false,
  },
};

ajv.addSchema([intSchema, strSchema]);
const validate = ajv.compile(mainSchema);
console.log(validate(dataA)); // true
console.log(validate(dataB)); // true
console.log(validate(dataC)); // false

Both dataA and dataB return true when I was expecting validate(dataB) to return false. I thought it would use the $schema value from the data to validate the object against the added schema with the corresponding $id.

I could make a function that steps through each data object and uses the $id property to validate it against the corresponding schema if that's the only way to accomplish this, but I assumed ajv should handle it.


Solution

  • As Jeremy Fiel pointed out, the validator ignores the $schema property in the nested data type (it didn't even have an issue with an undefined $schema being used). I played with the idea of using conditionals to pick what type of subschema to apply, but it seemed more straightforward to add validation rules to the $schema property itself.

    const ajv = new Ajv();
    
    const mainSchema = {
      $id: "mainSchema",
      type: "object",
      properties: {
        foo: {
          anyOf: [{ $ref: "intSchema" }, { $ref: "strSchema" }],
        },
      },
    };
    const intSchema = {
      $id: "intSchema",
      type: "object",
      properties: {
        $schema: {
          const: "intSchema",
        },
        value: {
          type: "integer",
        },
      },
    };
    const strSchema = {
      $id: "strSchema",
      type: "object",
      properties: {
        $schema: {
          const: "strSchema",
        },
        value: {
          type: "string",
        },
      },
    };
    
    const dataA = {
      $schema: "mainSchema",
      foo: {
        $schema: "intSchema",
        value: 42,
      },
    };
    
    const dataB = {
      $schema: "mainSchema",
      foo: {
        $schema: "strSchema",
        value: 1,
      },
    };
    
    const dataC = {
      $schema: "mainSchema",
      foo: {
        $schema: "unknownSchema",
        value: false,
      },
    };
    ajv.addSchema([intSchema, strSchema]);
    const validate = ajv.compile(mainSchema);
    console.log(validate(dataA)); // true
    console.log(validate(dataB)); // false
    console.log(validate(dataC)); // false
    

    The main downside to this strategy is that the $schema property loses some flexibility and it is a bit redundant.