Maks Babarowski
Maks Babarowski

Reputation: 672

How to conditionally forbid properties based on presence of other properties in JSON Schema?

In my schema I declared these properties:

"index_name": {
      "type": "string",
      "examples": ["foo-wwen-live", "foo"]
    },
"locale": {
      "type": "string",
      "examples": ["wwen", "usen", "frfr"]
},
"environment": {
      "type": "string",
      "default": "live",
      "examples": [
        "staging",
        "edgengram",
        "test"
      ]
}

I want a JSON body validated against my schema to be valid only if:

In short, locale and environment should never be mixed with index_name.

Test cases and desired results:

These should pass:
Case #1

{
  "locale": "usen"
}

Case #2

{
  "environment": "foo"
}

Case #3

{
  "environment": "foo",
  "locale": "usen"
}

Case #4

{
  "index_name": "foo-usen"
}

These should NOT pass:
Case #5

{
  "index_name": "foo-usen",
  "locale": "usen"
}

Case #6

{
  "index_name": "foo-usen",
  "environment": "foo"
}

Case #7

{
  "index_name": "foo-usen",
  "locale": "usen",
  "environment": "foo"
}

I created the following rule for my schema, however it does not cover all the cases. For example, if both locale and environment are present, validation returns failure if index_name is also present, which is correct behavior according to case #7. But if only one of locale and environment is present, it allows index_name to also be present (fails at cases #5 and #6).

  "oneOf": [
    {
      "required": ["index_name"],
      "not": {"required":  ["locale", "environment"]}
    },
    {
      "anyOf": [
        {
          "required": ["locale"],
          "not": {"required": ["index_name"]}
        },
        {
          "required": ["environment"],
          "not": {"required": ["index_name"]}
        }
      ]
    }
  ]

I'm getting mixed information on how "not": {"required": []} declaration works. Some people claim this means that it forbids anything declared in the array to be present, in contrary to what idea does the syntax give. Other claim that this should be taken exactly as it sounds: properties listed in the array are not required - they can be present, but it doesn't matter if they aren't.

Apart from this rule, I also require one non-related property to be present in all cases and I set "additionalProperties": false.

What is the rule that would satisfy all my test cases?

Upvotes: 11

Views: 3998

Answers (2)

Ethan
Ethan

Reputation: 725

since required: [a, b] means (a must be present AND b must be present)

then not: {required: [a, b]} means (NOT (a must be present AND b must be present))

which is logically equivalent to (a must NOT be present OR b must NOT be present).

so that is not the correct expression to say that (a must NOT be present AND b must NOT be present). you need two nots.

here is the correct expression, given your requirements:

{
  "oneOf": [
    {
      "required": ["index_name"],
      "allOf": [
        {"not": {"required": ["locale"]}},
        {"not": {"required": ["environment"]}}
      ]
    },
    {
      "anyOf": [
        {"required": ["locale"]},
        {"required": ["environment"]}
      ],
      "not": {
        "required": ["index_name"]
      }
    }
  ]
}

Upvotes: 1

Jason Desrosiers
Jason Desrosiers

Reputation: 24489

Dependencies

This is a job for the dependencies keyword. The following says

  • if "locale" is present, then "index_name" is forbidden.
  • if "environment" is present, then "index_name" is forbidden.

|

"dependencies": {
  "locale": { "not": { "required": ["index_name"] } },
  "environment": { "not": { "required": ["index_name"] } }
}

What's up with not-required?

There's a sub question about how not-required works. It's confusing because it doesn't mean how it reads in English, but it's similar enough to make us think it does sometimes.

In the above example, if we read it as "not required", it sounds like it means "optional". A more accurate description would be "forbidden".

That's awkward, but not too bad. Where it gets confusing is when you want to "forbid" more than one property. Let's assume we want to say, if "foo" is present, then "bar" and "baz" are forbidden. The first thing you might try is this.

"dependencies": {
  "foo": { "not": { "required": ["bar", "baz"] } }
}

However, what this says is that if "foo" is present, then the instance is invalid if both "bar" AND "baz" are present. They both have to be there to trigger failure. What we really wanted is for it to be invalid if "bar" OR "baz" are present.

"dependencies": {
  "foo": {
    "not": {
      "anyOf": [
        { "required": ["bar"] },
        { "required": ["baz"] }
      ]
    }
  }
}

Why is this so hard?

JSON Schema is optimized for schemas that are tolerant to changes. The schema should enforce that the instance has a the necessary data to accomplish a certain task. If it has more than it needs, the application ignores the rest. That way, if something is add to the instance, everything still works. It shouldn't fail validation if the instance has a few extra fields that the application doesn't use.

So, when you try to do something like forbidding things that you could otherwise ignore, you're going a bit against the grain of JSON Schema and things can get a little ugly. However, sometimes it's necessary. I don't know enough about your situation to make that call, but I'd guess that dependencies is probably necessary in this case, but additionalProperties is not.

Upvotes: 22

Related Questions