Amos Wesner
Amos Wesner

Reputation: 3

Setting additional properties based on other property values in JSON schema

I want to set some of the properties of a filter-item based on the value of an existing property. If the property Type has the value Range then the properties From and To should be added to the filterItem. Else the rest of the properties should be an array of FilterValues. I tried to set the properties with if and else, but it seems like I am missing on something.

The parts of my JSON schema:

"Filter": {
              "type": [
                "array",
                "null"
              ],
              "items": {
                "$ref": "#/definitions/filterItem"
              },
              "additionalProperties": false
            }
___________________________________________________

"filterItem": {
      "type": "object",
      "properties": {
        "AttributeCode": {
          "type": "string",
          "pattern": "^[a-z0-9_-]+$"
        },
        "Typ": {
          "type": "string"
        },
        "if": {
          "properties": {
            "Typ": {
              "const": "Range"
            }
          }
        },
        "then": {
          "properties": {
            "From": {
              "type": "integer",
              "minLength": 1
            },
            "To": {
              "type": "integer",
              "minLength": 1
            }
          },
          "additionalProperties": false
        },
        "else": {
          "properties": {
            "FilterValues": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^[a-z0-9_-]+$"
              }
            }
          },
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    }

The validator shows that the properties are not following my schema (Screenshot): validator

Upvotes: 0

Views: 2706

Answers (1)

Jason Desrosiers
Jason Desrosiers

Reputation: 24489

There are two major issues with your schema, how you use if-then-else and how you use addtionalProperties.

if-then-else

JSON Schema keywords can only appear in the context of a schema. properties defines an object whose values are a schema. properties itself is not a schema.

"properties": {
  "foo": { ... }
  "if": { ... },
  "then": { ... }
}

This doesn't define one property, "foo", plus a conditional. It defines three properties: "foo", "if", "then". You need to bring your conditional up a level for it to be recognized by the validator.

"properties": {
  "foo": { ... }
},
"if": { ... }
"then": { ... }

Otherwise, you're using if-then-else properly. It's just in the wrong place.

additionalProperties

Once you get your if-then-else in the right place, you'll notice that you get a bunch of additionalProperties errors. additionalProperties can take into account only the properties and patternProperties keywords in the same place in the schema.

{
  "allOf": [
    {
      "properties": {
        "foo": { ... }
      }
    },
    {
      "properties": {
        "bar": { ... }
      },
      "additionalProperties": false
    }
  ]
}

/allOf/1/additionalProperties can only take into account /allOf/1/properties, not /allOf/0/properties. So, { "foo": 1, "bar": 2 } will be invalid because "foo" is not defined in /allOf/1/properties.

There are a couple ways to deal with this. The preferred way is to simply not use additionalProperties. Ignore additional properties instead of forbidding them. This is best for evolvable schemas, but depending on the domain, it's not always feasible.

To use additionalProperties effectively, you need all possible property names under the same properties keyword.

"properties": {
  "foo": { ... },
  "bar": { ... }
},
"required": ["foo"],
"additionalProperties": false,
"if": {
  "properties": {
    "foo": { "const": 1 }
  },
  "required": ["foo"]
},
"then": { "required": ["bar"] }

This is one possible approach. "bar" is only expected if "foo" is 1, but "bar" is defined at the top and only required if the conditional passes. Not only does this make additionalProperties work as you expect, it's also easier to read because all of the property definitions are in one place and the conditional schemas are minimal.

At this point you might be concerned that including "bar" when "foo" is not 1 does not throw an error. Again, I'd encourage you to ignore the additional property rather than forbid it if your domain allows. But, if you really need to you can use else to forbid the extra field.

"else": { "not": { "required": ["bar"] } }

Be careful not to be confused by the words in that schema. It doesn't mean "not required" which sounds like "optional", it means "forbidden".

Upvotes: 1

Related Questions