Zohar Peled
Zohar Peled

Reputation: 82504

How to specify if a property should not exist or contain null?

A year ago I've asked how to set the type of a schema object based on the value of another property? which I've got a great answer for, and I've been using that schema ever since.

Now the source data have changed - and the schema is failing under the following circumstances:

The source data contains many properties, however only two of them are relevant for this question: "key" and "value" - the type of "value" depends on the value of "key" -

For instance:
If the key is "comment", the type of value {"Text":"commentValue"}.
If the key is "offset", the type of value is {"seconds":int}.
If the key is "weather", the type of value is {"value": Enum["sun", "clouds", "rain"...]}

Some of the keys do not have the value property, so the schema should forbid it from appearing with these keys - for instance, if the key is "standby" the value property should not appear at all - which is what my current schema is doing good.

However, now the data source have changed and I get "value" :{} as a part of my Json where once it was omitted - and the current schema does not allow it.

So my question is - how do I allow one of these two options? I've tried any combination of anyOf I could think of, but failed miserably - the Newtonsoft.Json.Schema.JSchema failed to parse the text.

Here's a simplified version of the schema I'm currently using:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "TestOptionalObject",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "test"
  ],
  "properties": {
    "test": {
      "$ref": "#/definitions/test"
    }
  },
  "definitions": {
    "test": {
      "type": "object",
      "required": [
        "key"
      ],
      "properties": {
        "key": {
          "type": "string",
          "enum": [
            "comment",
            "offset",
            "standby",
            "status_unsure",
            "status_ok"
          ]
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "event": {
                "enum": [
                  "standby",
                  "status_unsure",
                  "status_ok"
                ]
              }
            }
          },
          "then": {
            "properties": {
              "value": false
            }
          }
        },
        {
          "if": {
            "properties": {
              "key": {
                "const": "comment"
              }
            }
          },
          "then": {
            "properties": {
              "value": {
                "$ref": "#/definitions/commentValue"
              }
            }
          }
        },
        {
          "if": {
            "properties": {
              "key": {
                "const": "offset"
              }
            }
          },
          "then": {
            "properties": {
              "value": {
                "$ref": "#/definitions/offsetValue"
              }
            }
          }
        }
      ]
    },
    "commentValue": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "text"
      ],
      "properties": {
        "text": {
          "type": "string"
        }
      }
    },
    "offsetValue": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "seconds"
      ],
      "properties": {
        "seconds": {
          "type": "integer",
          "format": "int32"
        }
      }
    }
  }
}

Here are some of the things I've tried:

"then": {
  "properties": {
    "anyOf": [
      { "value": false },
      { "value": null }
    ]
  }
}
"then": {
  "anyOf": [
  {
    "properties": {
      { "value": false }
    },
    "properties": {
      { "value": null }
    }
  ]  
}
"then": {
  "properties": {
    "value": 
      "anyOf": [false, null ]
  }
}

Json examples to validate:

Should fail:

{
  "test": {
    "key": "comment",
      "value": {"seconds":12}
  }
}

{
  "test": {
    "key": "standby",
     "value": {"asdf":12}
  }
}

Should pass:

{
  "test": {
    "key": "comment",
     "value": {"text":"comment text"}
  }
}


{
  "test": {
    "key": "offset",
     "value": {"seconds":12}
  }
}

{
  "test": {
    "key": "standby"
  }
}

{
  "test": {
    "key": "standby",
     "value": {}
  }
}

Note the last example - the value property is an empty object - that also should pass but fails with the current schema since the value property should not exist at all for this key.

Upvotes: 2

Views: 3053

Answers (1)

Relequestual
Relequestual

Reputation: 12335

You're close, but not quite. You have to remember that the allOf is an array of subschemas (JSON Schemas). (Null isn't a valid schema, so you might have got some "not a valid schema" errors.)

As such, consider this modified subschema from allOf[0]...

{
  "if": {
    "properties": {
      "key": {
        "enum": [
          "standby",
          "status_unsure",
          "status_ok"
        ]
      }
    }
  },
  "then": {
    "properties": {
      "value": {
        "type": [
          "object"
        ],
        "additionalProperties": false
      }
    }
  }
}

You can test it here: https://jsonschema.dev/s/EfNI1

The if block remains the same. (Although I've corrected what I assume was a mistake in using event rather than key in your simplification from the real schema.)

The then block needs to define that the object (already checked by definitions.test) has a key of value, where the value of value is an object, and has no properties (aka an empty object).

To achive "an empty object", you need to use additionalProperties.

  • additionalProperties applies its value subschema to all properties that exist that haven't been defined in properties or that match the keys (regexes) from patternProperties.
  • false as a schema always fails validation.

additionalProperties without properites applies to ALL properties, and therefore with a value of false, validates "is an empty object".

Upvotes: 1

Related Questions