Zohar Peled
Zohar Peled

Reputation: 82474

how to set the type of a schema object based on the value of another property?

I have an object (from a 3rd party, so I can't change it) that have a property named "key", and another property called "value" that is optional, and it's type depends on the value of the "key" property.

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"...]}

Moreover, some of the keys do not have the value property, so the schema should forbid it from appearing with these keys. one of these keys is "standby" (as you can see in my current attempt below)

I've tried manipulating the code samples from this SO answer, but couldn't make it work.

I'm currently attempting to validate output json against my schema attempts using Newtonsoft's JSON Schema Validator - but I can't seem to get the "value" property defined correctly.

This is my code so far:

{
    "$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",
            "additionalProperties": false,
            "required": [
                "key",
            ],
            "properties": {
                "key": {
                    "type": "string",
                    "enum": ["standby", "comment", "offset"]
                },
                "value" : {
                    "if": {
                        "properties": {
                          "key": {"enum": ["comment"]}
                        }
                    },
                    "then": { 
                        "$ref": "#/definitions/commentValue"
                    },
                    "if": {
                        "properties": {
                          "key": {"enum": ["offset"]}
                        }
                    },
                    "then": { 
                        "$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"
                }
            }
        }
    }
}

And this is the error messages I get:

JSON does not match schema from 'then'. Schema path: #/definitions/offsetValue/then

Property 'text' has not been defined and the schema does not allow additional properties. Schema path: #/definitions/offsetValue/additionalProperties

Required properties are missing from object: seconds. Schema path: #/definitions/offsetValue/required

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}
  }
}

Upvotes: 3

Views: 2946

Answers (2)

Biller Builder
Biller Builder

Reputation: 323

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"...]}

This is a classic case of discriminated union, so can be expressed like this:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "anyOf": [
    {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "key": {
          "const": "comment"
        },
        "value": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "Text": {
              "type": "string"
            }
          }
        }
      }
    },
    {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "key": {
          "const": "offset"
        },
        "value": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "seconds": {
              "type": "integer"
            }
          }
        }
      }
    },
    {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "key": {
          "const": "weather"
        },
        "value": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "value": {
              "enum": ["sun", "clouds", "rain"]
            }
          }
        }
      }
    },
    {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "key": {
          "enum": ["standby"]
        }
      }
    }
  ]
}

It can probably be simplified further, but you get the idea.
Generally avoid using fancy JSON Schema features which don't map directly to basic type systems unless you absolutely have to. The whole point of JSON Schema is to represent serializable data in readable form both for machines and humans, not map the type system of the language running the server into JSON.

Upvotes: 1

Relequestual
Relequestual

Reputation: 12315

I have changed your JSON Schema so it does what you expect, apart form key of standby as you didn't include that in your schema, and you should be able to replicate the pattern I've created to add new keys as required.

The major issue you had was a false assumption about where to place if/then/else keywords. They are applicator keywords, and so must be applied to the object which you are checking the condition of, and not a properties key value. Because you were using if/then/else in the object which was a value of value, you were applying if/then/else to the value of value rather than test.

You needed your if to apply to test to get the correct scope for checking the key property value.

Here is the resulting fixed schema:

{
  "$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": [
            "standby",
            "comment",
            "offset"
          ]
        }
      },
      "allOf": [
        {
          "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"
        }
      }
    }
  }
}

If you want any more help, please feel free to join the JSON Schema slack using the discussion link on the http://json-schema.org site.

Upvotes: 3

Related Questions