Json Schema Different required properties for first element in array? - jsonschema

Suppose I want to validate an array in json where only the first element has an optional property. The rest of the items in the array would required the property.
schema.json
{
"type": "object",
"properties": {
"x": {
"type": "array",
"items": {
"oneOf": [
{ "$ref": "./first.json" },
{ "$ref": "./rest.json" }
]
}
}
}
}
first.json
{
"type": "object",
"properties": {
"y": { "type": "number" },
"z": { "type": "boolean" }
},
"required": ["y"]
}
rest.json
{
"type": "object",
"properties": {
"y": { "type": "number" },
"z": { "type": "boolean" }
}
}
Valid json:
{
"x": [
{ "z": true },
{ "y": 4, "z": true },
{ "y": 5 }
]
}
Invalid json:
{
"x": [
{ "z": true },
{ "z": true }, // this line would be invalid
{ "y": 5 }
]
}
However, the above setup gives me "multiple schemas match" when I provide the y value. Another approach could be:
schema.json
{
"type": "object",
"properties": {
"x": {
"type": "array",
"items": [
{ "$ref": "./first.json" },
{ "$ref": "./rest.json" }
]
}
}
}
This schema does what I want for the first two elements in the array (not requiring y in the first but yes in the second), but does not apply any schema to the objects after the second.

The items keyword has two forms. The first is the schema-form that you are using. The second is the array-form. The array-form takes an array of schemas where each schema applies to it's corresponding item in the instance. For example, when given an array with two schema, it validates the first item in the instance against the first schema and the second item against the second schema.
There is also an additionalItems keyword that works with the array-form of items. Any item that doesn't correspond to a schema in items is validated against the additionalItems schema. Using the previous example with two schemas in items, an instance with three items would have it's third item evaluated against the additionalItems schema.
So, we can use the array-form of items to apply "./first.json" to the first items and use additionalItems to apply "./rest.json" to the rest of the array.
{
"type": "object",
"properties": {
"x": {
"type": "array",
"items": [{ "$ref": "./first.json" }],
"additionalItems": { "$ref": "./rest.json" }
}
}
}

Related

Json Schema Validation of Array or Objects

I have a set of options for validation:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "parsePosition Validator",
"type": "object",
"properties": {
"option1": {
"type": "array",
"items": {
"tpye": "number"
},
"minItems": 3
},
"option2": {
"type": "array",
"items": {
"tpye": "number"
},
"minItems": 3
},
"option3": {
"oneOf": [
{
"type": "array",
"items": {
"tpye": "number"
},
"minItems": 3
},
{
"x": { "type":"number" },
"y": { "type":"number" },
"z": { "type":"number" },
"required": ["x","y","z"]
}
}
},
"oneOf": [
{ "required":["option1"] },
{ "required":["option2"] },
{ "required":["option3"] }
]
}
Essentially the input of the json can be any of the three options. My issue arises when trying to validate option3. Possible inputs could be:
{
"option3": [1,2,3]
}
or
{
"option3": {
"x": 1,
"y": 2,
"z": 3
}
}
However, when I use the array rather than objects, the validator does not throw an issue with an empty array or an array of incorrect size.
Am I missing something?
You are missing something very simple...
You haven't put type: object in your second subschema in the oneOf. for option 3.
The reason you need this is because JSON Schema is a constraints based language.
Specifying properties, doesn't mean the the instance location HAS to be an object; You also need to say "this location should be an object".
An array of any size is valid under your subschema oneOf[1], because properties and `required are only applicable to an object.

JSON Schema switch object properties based on enum

I've been struggling with "switch" in JSON Schema. Went through couple of GitHub and SO discussions on this topic but haven't find solution.
My intention is to vary "payload" object properties based on "id" enum that will have 30 different mappings ("payload" definitions per enum "id").
For example first message json object will have amount and other properties but for the demo purpose let's go only with one property (amout):
{
"message": {
"id": 1,
"correlationId": "a0011e83-280e-4085-b0f1-691059aaae61",
"payload": {
"amount": 100
}
}
}
And second json:
{
"message": {
"id": 2,
"correlationId": "a0011e83-280e-4085-b0f1-691059aaae61",
"payload": {
"code": "xyz"
}
}
}
Is there a way to build JSON Schema (draft 7 or any other) in this manner?
What you're asking for is a fairly common requirement. Using oneOf/anyOf should get you where you want.
In those cases where the alternatives are mutually exclusive (due to the different "id" values), I'm in favour of anyOf to allow Schema Validator to stop checking when encountering the first matching subschema – whereas oneOf implies that all other alternatives must not match, e.g. in case of "id": 1 a validator would only have to check against the first subschema in an anyOf to indicate that it is valid while for oneOf it'd have to check against the other 29 to ensure that those aren't also valid. But you may find oneOf more expressive for human consumers of your schema.
For your particular scenario, I'd imagine something along the lines of the following schema:
{
"type": "object",
"required": ["message"],
"properties": {
"message": {
"type": "object",
"required": ["id", "correlationId", "payload"],
"properties": {
"id": { "enum": [1, 2, 3] },
"correlationId": { "type": "string" },
"payload": { "type": "object" }
},
"anyOf": [
{
"properties": {
"id": { "const": 1 },
"payload": { "$ref": "#/definitions/payload1" }
}
},
{
"properties": {
"id": { "const": 2 },
"payload": { "$ref": "#/definitions/payload2" }
}
},
{
"properties": {
"id": { "const": 3 },
"payload": { "$ref": "#/definitions/payload3" }
}
},
]
}
},
"definitions": {
"payload1": {
"type": "object",
"required": ["amount"],
"properties": {
"amount": { "type": "integer" }
}
},
"payload2": {
"type": "object",
"required": ["code"],
"properties": {
"code": { "type": "string" }
}
},
"payload3": {
"type": "object",
"required": ["foo"],
"properties": {
"foo": { "type": "string" }
}
}
}
}

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

JSON SCHEMA - How can i check a values exists in an array

I have a simple question. I have a property groupBy which is an array and contain only two possible values "product" and "date". Now i want to make another property required based upon a value exists in the groupBy array. In this case when my groupBy array contains "date" i want to make resolution required! How can i do that ?
Who can i check if an array contains a value ?
var data = {
"pcsStreamId": 123123,
"start": moment().valueOf(),
"end": moment().valueOf(),
"groupBy" : ["product"]
};
var schema = {
"type": "object",
"properties": {
"pcsStreamId": { "type": "number" },
"start": { "type": "integer", "minimum" : 0 },
"end": { "type": "integer", "minimum" : 0 },
"groupBy": {
"type": "array",
"uniqueItems": true,
"items" : {
"type": "string",
"enum": ["product", "date"]
},
"oneOf": [
{
"contains": { "enum": ["date"] },
"required": ["resolution"]
}
]
},
"resolution" : {
"type": "string",
"enum": ["day", "year", "month", "shift"]
},
},
"required": ["pcsStreamId", "start", "end", "groupBy"]
};
To solve the problem we have to use a boolean logic concept called implication. To put the requirement in boolean logic terms, we would say "groupBy" contains "date" implies that "resolution" is required. Implication can be expressed as "(not A) or B". In other words, either "groupBy" does not contain "date", or "resolution" is required. In this form, it should be more clear how to implement the solution.
{
"type": "object",
"properties": {
"pcsStreamId": { "type": "number" },
"start": { "type": "integer", "minimum": 0 },
"end": { "type": "integer", "minimum": 0 },
"groupBy": {
"type": "array",
"uniqueItems": true,
"items": { "enum": ["product", "date"] }
},
"resolution": { "enum": ["day", "year", "month", "shift"] }
},
"required": ["pcsStreamId", "start", "end", "groupBy"],
"anyOf": [
{ "not": { "$ref": "#/definitions/contains-date" } },
{ "required": ["resolution"] }
],
"definitions": {
"contains-date": {
"properties": {
"groupBy": {
"contains": { "enum": ["date"] }
}
}
}
}
}
Edit
This answer uses the new draft-06 contains keyword. I used it because the questioner used it, but if you are on draft-04, you can use this definition of "contains-date" instead. It uses another logic identity (∃x A <=> ¬∀x ¬A) to get the functionality of the contains keyword.
{
"definitions": {
"contains-date": {
"properties": {
"groupBy": {
"not": {
"items": {
"not": { "enum": ["date"] }
}
}
}
}
}
}
}
Your groupby property seems to be an object. May be its the nested enum property that you are talking about ( since that is an array and contains date and product). However, once you parsed this json to an object you can check something like:
groupby.items.enums.indexOf('date') === -1
array.indexOf(anyItem) returns the index of the item, if present in the array else it returns -1.
Hope that helps

json schema object property constraints

In my schema I have an array of phone objects. Each object has a "status" property, which can be one of three values: "Primary", "Active" and "Not-in-use".
I want to set the following constraint:
If the number of phone objects > 0 then exactly one must have status="Primary"
Is this possible with json schema? If so, how?
This schema is pretty close to what you want. The only restriction is that the "Primary" phone number needs to be the first item in the array.
You might be able to get "Primary" to be anywhere in the array with some creative use of not. I'll update the answer if I figure it out.
{
"type": "object",
"properties": {
"phoneNumbers": {
"type": "array",
"items": [{ "$ref": "#/definitions/primaryPhone" }],
"additionalItems": { "$ref": "#/definitions/additionalPhone" }
}
},
"definitions": {
"phone": {
"type": "object",
"properties": {
"label": { "type": "string" },
"number": { "type": "string" }
},
"required": ["label", "number", "status"]
},
"primaryPhone": {
"allOf": [{ "$ref": "#/definitions/phone" }],
"properties": {
"status": { "enum": ["Primary"] }
}
},
"additionalPhone": {
"allOf": [{ "$ref": "#/definitions/phone" }],
"properties": {
"status": { "enum": ["Active", "Not-in-use"] }
}
}
}
}