2

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}
  }
}
Zohar Peled
  • 79,642
  • 10
  • 69
  • 121
  • 1
    I'm not able to test this, but I'm 99% sure the issue you have here is you have multiple `if/then` statements in the same object. Remember, the behaviour of a JSON object which has duplicate keys is undefined. You can either wrap each pair in an `allOf`, or use the `else` keyword and define the next schema inside that. – Relequestual Dec 21 '18 at 08:59
  • putting the second `if` inside the `else` of the first one was a partial success - I've successfully validated a json with `comment` key but failed to validate a json with `offset` key. Will try the `allOf` and report. – Zohar Peled Dec 21 '18 at 10:01
  • Nesting `if/then/else` gets annoying if you have many checks to do. `oneOf` may be faster if your library of choice short-circuits. – Relequestual Dec 21 '18 at 10:03
  • Couldn't get oneOf or allOf to work properly. The schema is valid but it passes invalid json outputs as valid... – Zohar Peled Dec 21 '18 at 10:07
  • Can you simplify your new JOSN Schema and JSON instance and update your question to include please? I can then help. Give example of data that should pass and should fail. – Relequestual Dec 21 '18 at 10:09
  • @Relequestual I'm not sure how to simplify the schema, so I've left it as is, but I've added sample jsons that should pass or fail. Note: the one with the key "standby" should have no value property at all. Thanks! – Zohar Peled Dec 21 '18 at 10:14
  • OK. I can immediately see some problems. Try your schema and instances using https://www.jsonschemavalidator.net and see if you can sort out the issues. Initially, `seconds` causes validation to fail because `additionalProperties` is false for the `test` object. – Relequestual Dec 21 '18 at 10:18
  • I've already tested it with jsonschemavalidator.net... and the seconds is a property of the `value` object, not of the `test` object, so as far as I understand it should not be a problem... – Zohar Peled Dec 21 '18 at 11:04
  • I'm looking at the first example instance that you say should pass. Otherwise, your provided instances pass and fail as expected. – Relequestual Dec 21 '18 at 11:07
  • Sorry, this is a typo. fixed. – Zohar Peled Dec 21 '18 at 11:12
  • Right, I can see what you've done / overlooked. There are a few issues in your schema creation. I'll have to wait a bit before I can restructure this, but I can see what you want and why it's not working. =] Expect something in about 1 hour. – Relequestual Dec 21 '18 at 11:15

2 Answers2

3

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.

Relequestual
  • 11,631
  • 6
  • 47
  • 83
  • Thanks! I figured it's something simple that I'm missing.... Just one more thing is needed for this to be perfect for my needs - when the key is "standby", the validation should fail if the json output contains the "value" property. – Zohar Peled Dec 21 '18 at 12:31
  • 1
    You can do this by adding another `if/then` block, where the `then` schema is: properties -> value -> false – Relequestual Dec 21 '18 at 13:02
  • 1
    Thanks a lot! You've probably saved me a day of work on this! – Zohar Peled Dec 21 '18 at 13:17
  • 1
    I'll know where to come if I have an SQL issue ;) You're welcome! =] – Relequestual Dec 21 '18 at 13:19
1

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.

Biller Builder
  • 303
  • 3
  • 10
  • Thank you for your answer, though too late for me to test it I'm pretty sure someone might find it useful in the future. – Zohar Peled Aug 31 '23 at 10:39