Search code examples
jsonjsonschemaxapi

Why does my JSON Schema appear to not validate a child object correctly?


Given the JSON and Schema below, actor.mbox, actor.member[0].objectType and actor.member[0].mbox should all fail. They do not. There must be something wrong in my schema. I think I have narrowed it to something dealing with the IdGroup definition but I cannot find the problem. Any json schema guru see anything obviously wrong?

JSON

{
  "actor": {
    "objectType": "Group",
    "name": "Group Identified",
    "mbox": "http://should.fail.com",
    "member": [
      {
        "objectType": "Agent_shouldfail",
        "name": "xAPI mbox",
        "mbox": "mailto:shouldfail"
      }
    ]
  },
  "verb": {
    "id": "http://adlnet.gov/expapi/verbs/attended",
    "display": {
      "en-GB": "attended",
      "en-US": "attended"
    }
  },
  "object": {
    "objectType": "Activity",
    "id": "http://www.example.com/meetings/occurances/34534"
  }
}

JSON Schema (stripped down)

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "xAPIValidator",
  "description": "Validation schema for xAPI tests",
  "type": "object",
  "allOf": [
    {
      "$ref": "#/definitions/Statement"
    }
  ],
  "statements": {
    "type": "array",
    "items": {
      "allOf": [
        {
          "$ref": "#/definitions/Statement"
        }
      ]
    }
  },
  "definitions": {
    "Statement": {
      "$id": "#Statement",
      "additionalProperties": false,
      "properties": {
        "objectType": {
          "type:": "string",
          "enum": [
            "Agent",
            "Activity",
            "Group",
            "SubStatement",
            "StatementRef"
          ]
        },
        "id": {
          "allOf": [
            {
              "$ref": "#/definitions/uuid"
            }
          ]
        },
        "timestamp": {
          "allOf": [
            {
              "$ref": "#/definitions/timestamp"
            }
          ]
        },
        "stored": {
          "allOf": [
            {
              "$ref": "#/definitions/timestamp"
            }
          ]
        },
        "version": {
          "allOf": [
            {
              "$ref": "#/definitions/semanticVersion"
            }
          ]
        },
        "actor": {
          "$id": "#actor",
          "allOf": [
            {
              "$ref": "#/definitions/allOfAgentGroup"
            }
          ]
        },
        "authority": {
          "allOf": [
            {
              "$ref": "#/definitions/allOfAgentGroup"
            }
          ]
        },
        "verb": {
          "$id": "#verb",
          "type": "object",
          "properties": {
            "id": {
              "allOf": [
                {
                  "$ref": "#/definitions/URI"
                }
              ]
            },
            "display": {
              "type": "object",
              "allOf": [
                {
                  "$ref": "#/definitions/lang5646"
                }
              ]
            }
          }
        },
        "object": {
          "$id": "#object",
          "type": "object",
          "additionalProperties": true,
          "properties": {
            "objectType": {
              "type:": "string",
              "enum": [
                "Activity",
                "Agent",
                "Group",
                "SubStatement",
                "StatementRef"
              ]
            }
          }
        }
      },
      "required": [
        "actor",
        "verb",
        "object"
      ]
    },
    "attachment": {
      "properties": {
        "usageType": {
          "allOf": [
            {
              "$ref": "#/definitions/URI"
            }
          ]
        },
        "display": {
          "allOf": [
            {
              "$ref": "#/definitions/lang5646"
            }
          ]
        },
        "description": {
          "allOf": [
            {
              "$ref": "#/definitions/lang5646"
            }
          ]
        },
        "contentType": {
          "type": "string",
          "pattern": "\\w+/[-+.\\w]+;?(\\w+.*=\\w+;?)*"
        },
        "length": {
          "type": "integer"
        },
        "sha2": {
          "type": "string"
        },
        "fileUrl": {
          "allOf": [
            {
              "$ref": "#/definitions/URI"
            }
          ]
        }
      }
    },
    "semanticVersion": {
      "type": [
        "string"
      ],
      "pattern": "^([0-9]+)\\.([0-9]+)\\\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$"
    },
    "Agent": {
      "$id": "#Agent",
      "allOf": [
        {
          "$ref": "#/definitions/IFI"
        }
      ]
    },
    "AnonGroup": {
      "$id": "#AnonGroup",
      "maxProperties": 3,
      "properties": {
        "member": {
          "type": "array",
          "items": [
            {
              "allOf": [
                {
                  "$ref": "#/definitions/allOfAgentGroup"
                }
              ]
            }
          ]
        }
      },
      "dependencies": {
        "objectType": [
          "member"
        ]
      },
      "required": [
        "member"
      ],
      "not": {
        "required": [
          "mbox"
        ]
      },
      "not": {
        "required": [
          "mbox_sha1sum"
        ]
      },
      "not": {
        "required": [
          "openid"
        ]
      },
      "not": {
        "required": [
          "account"
        ]
      }
    },
    "IdGroup": {
      "$id": "#IdGroup",
      "properties": {
        "member": {
          "type": "array",
          "items": [
            {
              "allOf": [
                {
                  "$ref": "#/definitions/allOfAgentGroup"
                }
              ]
            }
          ]
        }
      },
      "allOf": [
        {
          "$ref": "#/definitions/IFI"
        }
      ]
    },
    "allOfAgentGroup": {
      "properties": {
        "objectType": {
          "type": "string",
          "enum": [
            "Agent",
            "Group"
          ]
        },
        "name": {
          "type": "string"
        }
      },
      "oneOf": [
        {
          "if": {
            "properties": {
              "objectType": {
                "const": "Agent"
              }
            }
          },
          "then": {
            "allOf": [
              {
                "$ref": "#/definitions/Agent"
              }
            ]
          }
        },
        {
          "if": {
            "properties": {
              "objectType": {
                "const": "Group"
              }
            }
          },
          "then": {
            "oneOf": [
              {
                "allOf": [
                  {
                    "$ref": "#/definitions/IdGroup"
                  }
                ]
              },
              {
                "allOf": [
                  {
                    "$ref": "#/definitions/AnonGroup"
                  }
                ]
              }
            ]
          }
        }
      ]
    },
    "IFI": {
      "oneOf": [
        {
          "properties": {
            "mbox": {
              "allOf": [
                {
                  "$ref": "#/definitions/mailto"
                }
              ]
            }
          },
          "required": [
            "mbox"
          ]
        },
        {
          "properties": {
            "mbox_sha1sum": {
              "type": "string",
              "pattern": "\\b[0-9a-f]{5,40}\\b"
            }
          },
          "required": [
            "mbox_sha1sum"
          ]
        },
        {
          "properties": {
            "account": {
              "properties": {
                "homePage": {
                  "allOf": [
                    {
                      "$ref": "#/definitions/URI"
                    }
                  ]
                },
                "name": {
                  "type": "string"
                }
              },
              "required": [
                "homePage",
                "name"
              ]
            }
          },
          "required": [
            "account"
          ]
        },
        {
          "properties": {
            "openid": {
              "allOf": [
                {
                  "$ref": "#/definitions/URI"
                }
              ]
            }
          },
          "required": [
            "openid"
          ]
        }
      ]
    },
    "mailto": {
      "type": "string",
      "pattern": "(mailto:)(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"
    },
    "timestamp": {
      "type": "string",
      "pattern": "^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$"
    },
    "URI": {
      "type": "string",
      "pattern": "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"
    },
    "uuid": {
      "type": "string",
      "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
    },
    "lang5646": {
      "type": "object",
      "patternProperties": {
        "^((?:(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?:([A-Za-z]{2,3}(-(?:[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?:[A-Za-z]{4}))?(-(?:[A-Za-z]{2}|[0-9]{3}))?(-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?:[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?:x(-[A-Za-z0-9]{1,8})+))?)|(?:x(-[A-Za-z0-9]{1,8})+))$": {
          "type": "string"
        }
      },
      "additionalProperties": false
    },
    "lang5646string": {
      "type": "string",
      "pattern": "^((?:(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?:([A-Za-z]{2,3}(-(?:[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?:[A-Za-z]{4}))?(-(?:[A-Za-z]{2}|[0-9]{3}))?(-(?:[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?:[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?:x(-[A-Za-z0-9]{1,8})+))?)|(?:x(-[A-Za-z0-9]{1,8})+))$"
    }
  }
}

Solution

  • if/then/else doesn't quite work as you're expecting.

    In your schema definition allOfAgentGroup has a oneOf section. Let's look at that on it's own.

    In your sample data that should fail, let's take the "actor" object on it's own also.

    Schema:

    {
      "oneOf": [
        {
          "if": {
            "properties": {
              "objectType": {
                "const": "Agent"
              }
            }
          },
          "then": false
          }
        },
        false
      ]
    }
    

    Instance data:

    {
      "objectType": "Group",
      "name": "Group Identified",
      "mbox": "http://should.fail.com",
      "member": [
        {
          "objectType": "Agent_shouldfail",
          "name": "xAPI mbox",
          "mbox": "mailto:shouldfail"
        }
      ]
    }
    

    We know you expect the second part item in the oneOf array to fail validation. For debugging and demonstration purposes, let's assume it does fail, and change it to false (which is a valid "JSON Schema", always causing validation to fail on that branch).

    Now, given the above schema and instance, you would expect the JSON instance data to fail validation, right? It does not, and in fact validation passes.

    Remember, each JSON Schema keyword adds constraints to your validation requirements. Let's look at what if and then actually DO.

    if

    This validation outcome of this keyword's subschema has no direct effect on the overall validation result. Rather, it controls which of the "then" or "else" keywords are evaluated.

    Instances that successfully validate against this keyword's subschema MUST also be valid against the subschema value of the "then" keyword, if present.

    http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.6

    OK, so if the schema of if successfully validates, the schema from then is applied.

    The instance data fails the if condition validation, and so... the schema from then is not applied, and you end up with what is effectivly an "empty schema", which means no validation constraints for oneOf/0. This results in your oneOf asserting validation is successfull, because oneOf/0 passes and oneOf/1 fails.

    To solve this, you need to add "then": false to your object which contains if and then, in the intance where you want that schema item to fail validation if the if condition is not met.