Use conditional statements on json schema based on another schema object - json

I have a json object like:
{
"session": {
"session_id": "A",
"start_timestamp": 1535619633301
},
"sdk": {
"name": "android",
"version": "21"
}
}
The sdk name can either be android or ios. And the session_id is based on name field in sdk json. I have written a json schema using conditional statement (Using draft 7) as follows:
But it works in an unexpected manner:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/Base",
"definitions": {
"Base": {
"type": "object",
"additionalProperties": false,
"properties": {
"session": {
"$ref": "#/definitions/Session"
},
"sdk": {
"$ref": "#/definitions/SDK"
}
},
"title": "Base"
},
"Session": {
"type": "object",
"additionalProperties": false,
"properties": {
"start_timestamp": {
"type": "integer",
"minimum": 0
},
"session_id": {
"type": "string",
"if": {
"SDK": {
"properties": {
"name": {
"enum": "ios"
}
}
}
},
"then": {
"pattern": "A"
},
"else": {
"pattern": "B"
}
}
},
"required": [
"session_id",
"start_timestamp"
],
"title": "Session"
},
"SDK": {
"type": "object",
"additionalProperties": false,
"properties": {
"version": {
"type": "string"
},
"name": {
"type": "string",
"enum": [
"ios",
"android"
]
}
},
"required": [
"name",
"version"
],
"title": "SDK"
}
}
}
So the following JSON Passes:
{
"session": {
"session_id": "A",
"start_timestamp": 1535619633301
},
"sdk": {
"name": "ios",
"version": "21"
}
}
But this fails:
{
"session": {
"session_id": "B",
"start_timestamp": 1535619633301
},
"sdk": {
"name": "android",
"version": "21"
}
}
can someone explain y?.. Even this passes:
{
"session": {
"session_id": "A",
"start_timestamp": 1535619633301
},
"sdk": {
"name": "android",
"version": "21"
}
}

I think you're having a similar problem as in this question.
#Relequestual is right in that you need the properties keyword around your SDK callout. But for what you want to do, you need to reorganize.
Subschemas only operate on their level in the instance, not at the root.
Consider this schema for a simple JSON object instance containing a one and a two property:
{
"properties": {
"one": {
"enum": ["yes", "no", "maybe"]
},
"two": {
"if": {
"properties": {
"one": {"const": "yes"}
}
},
"then": {
... // do some assertions on the two property here
},
"else": {
...
}
}
}
}
The if keyword under the two property can only consider the portion of the instance under the two property (i.e. two's value). It's not looking at the root of the instance, so it can't see the one property at all.
To make it so that the subschema under the two property subschema can see the one property in the instance, you have to move the if outside of the properties keyword.
{
"if": {
"properties": {
"one": {"const" : "yes"}
}
},
"then": {
... // do some assertions on the two property here
},
"else": {
... // assert two here, or have another if/then/else structure to test the one property some more
}
}
For two possible values of one, this is pretty good. Even three possible values isn't bad. However, as the possible values of one increases, so does the nesting of ifs, which can make your schema horrible to read (and possibly make validation slower).
Instead of using the if/then/else construct, I suggest using an anyOf or oneOf where each subschema represents a valid state for the instance, given the varying values of one.
{
"oneOf": [
{
"properties": {
"one": {"const": "yes"},
"two": ... // do some assertions on the two property here
}
},
{
"properties": {
"one": {"const": "no"},
"two": ... // do some assertions on the two property here
}
},
{
"properties": {
"one": {"const": "maybe"},
"two": ... // do some assertions on the two property here
}
}
]
}
This is much cleaner in my opinion.
Hopefully that explanation helps you reconstruct your schema to allow those other instances to pass.

You have to move your conditional to a high enough level to be able to reference all of the the properties it needs to reference. In this case, that's the /definitions/Base schema. Then you just need to write your schemas properly as Relequestual explained.
{
"$ref": "#/definitions/Base",
"definitions": {
"Base": {
"type": "object",
"properties": {
"session": { "$ref": "#/definitions/Session" },
"sdk": { "$ref": "#/definitions/SDK" }
},
"allOf": [
{
"if": {
"properties": {
"sdk": {
"properties": {
"name": { "const": "ios" }
}
}
},
"required": ["sdk"]
},
"then": {
"properties": {
"session": {
"properties": {
"session_id": { "pattern": "A" }
}
}
}
},
"else": {
"properties": {
"session": {
"properties": {
"session_id": { "pattern": "B" }
}
}
}
}
}
]
},
...
}

The value of if must be a JSON Schema. If you were to take lines https://gist.github.com/Relequestual/f225c34f6becba09a2bcaa66205f47f3#file-schema-json-L29-L35 (29-35) and use that as a JSON Schema by itself, you would impose no validation constraints, because there are no JSON Schema key words at the top level of the object.
{
"SDK": {
"properties": {
"name": {
"enum": "ios"
}
}
}
}
This is allowed in the specification, because people may want to extend the functionality of JSON Schema by adding their own key words. So it's "valid" JSON Schema, but doesn't actually DO anything.
You Need to add properties to the schema for it to make sense.
{
"properties": {
"SDK": {
"properties": {
"name": {
"const": "ios"
}
}
}
}
}
Additionally, enum must be an array. When you only have a single item, you may use const.

Related

Json Schema for Aspect Oriented Json files

So my json structure is aspect oriented, meaning that the json is structure in a way that each data is represented by a key and that key will define the structure of its content.
for example:
[
{
"nv": [{ "ln": 123 }]
},
{
"metadata": [{ "name": "nodes" }, { "name": "edges" }]
},
{
"nodes": [{ "#id": 1 }, { "#id": 2 }]
},
{
"edges": [
{ "#id": 1, "nodeId": 1 },
{ "#id": 2, "nodeId": 2 }
]
},
{
"status": [{ "success": true }]
}
]
As shown 3 objects (nv, metadata, status) and based on the name inside of the metadata there will be objects inside the json file.
I tried something like this:
{
"type": "array",
"items": [
{
"type": "object",
"properties": {
"nv": { "type": "array", "items": { "$ref": "#definitions/nv" } }
},
"required": ["nv"]
},
{
"type": "object",
"properties": {
"metaData": {
"type": "array",
"items": { "$ref": "#definitions/metadata" }
}
},
"required": ["metaData"]
},
{
"anyOf": [
{
"type": "object",
"properties": {
"nodes": {
"type": "array",
"items": { "$ref": "#definitions/nodes" }
}
}
},
{
"type": "object",
"properties": {
"edges": {
"type": "array",
"items": { "$ref": "#definitions/edges" }
}
}
},
{
"type": "object",
"properties": {
"edgeAttribute": {
"type": "array",
"items": { "$ref": "#definitions/edgeAttribute" }
}
}
},
{
"type": "object",
"properties": {
"nodeAttribute": {
"type": "array",
"items": { "$ref": "#definitions/nodeAttribute" }
}
}
}
]
},
{
"type": "object",
"properties": {
"status": {
"type": "array",
"items": { "$ref": "#definitions/status" }
}
},
"required": ["status"]
}
],
"definitions": {
"status": {
"type": "object",
"properties": {
"success": { "type": "boolean" }
}
"etc..."
}
}
}
but then if I define an empty array it will be accepted, also it is being accepted if the array only contains one of the 3 required objects.
So is there a way to validate against something like the example using json-schemas?
The real scenario may have more than just 2 objects inside of the metadata that's why I did not use if -> then -> else conditions. if the solution is by using them then please let me know.
The structure of the data makes this a rough one, but there are a few patterns you can use to get the behavior you want. Let's take them one at a time.
Declare an array that can contain any of a number of objects
Generally people use oneOf for this, but I don't recommend that because it can have poor performance and incomprehensible error messages. Usually that means if/then, but in this case you can get good results by defining your items as a single object that only allows one property at a time in each object.
{
"items": {
"type": "object",
"properties": {
"nv": { "$ref": "#/definitions/nv" },
"metadata": { "$ref": "#/definitions/metadata" },
"status": { "$ref": "#/definitions/status" },
"nodes": { "$ref": "#/definitions/nodes" },
"edges": { "$ref": "#/definitions/edges" }
},
"minProperties": 1,
"maxProperties": 1
}
}
Edit: Previously, I recommended dependencies, but then realized that this is better.
Assert that the array contains a required object
To do this, you need to assert that the array contains an object that has a required property.
{ "contains": { "type": "object", "required": ["nv"] } }
You'll have to combine this pattern in allOf to express additional required items.
{
"allOf": [
{ "contains": { "type": "object", "required": ["nv"] } },
{ "contains": { "type": "object", "required": ["metadata"] } },
{ "contains": { "type": "object", "required": ["status"] } }
]
}
Conditionally assert that the array contains a required object
The tricky part here is getting all the nested contains and properties in the if to be able to assert that the "name" is a certain value. The then just uses the same pattern we used above to assert than an object is required in the array.
{
"if": {
"type": "array",
"contains": {
"type": "object",
"properties": {
"metadata": {
"type": "array",
"contains": {
"type": "object",
"properties": {
"name": { "const": "nodes" }
},
"required": ["name"]
}
}
},
"required": ["metadata"]
}
},
"then": { "contains": { "type": "object", "required": ["nodes"] } }
}
The above example shows the "nodes" object being conditionally required. You'll need to repeat this pattern for the "edges" object and combine them with allOf. I suggest making use of definitions to help make this readable.
{
"allOf": [
{ "$ref": "#/definitions/if-metadata-has-nodes-then-require-nodes-object" },
{ "$ref": "#/definitions/if-metadata-has-edges-then-require-edges-object" }
]
}
I would suggest moving each of your "types" into a $defs to be referenced.
{
"$defs": {
"nvObj": {
"type": "object",
"properties": {
"nv": { "type": "array", "items": { "$ref": "#/$defs/nv" } }
},
"required": ["nv"]
},
... // other defitions
}
}
(I've updated the $ref to use $defs instead of definitions as this is the new keyword since draft 7.)
Then you can put many references into a oneOf.
{
"$defs": {
... // from above
},
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/$defs/nvObj" },
... // all of the other object definitions
]
}
}
You're right to avoid if/then/else for this case. oneOf is the best bet here.

JSON Schema - combining allOf and anyOf to make a flexible schema

In plain English, I want a flexible schema that will allow an object called "message" to contain a couple of string props, and then a 3rd prop that can be either a plain string or another object. So, if it's defined thusly, is this achieving the goal or is there a validation gotcha that I am missing?
"message": {
"type": "object",
"properties": {
"another": {
"$ref": "another.schema.json"
},
"#type": {
"type": "string",
"enum": [
"One",
"Two",
"Three"
]
}
},
"allOf": [
{
"$ref": "#message"
},
{
"anyOf": [
{
"plainolstring": {
"type": "string"
}
},
{
"obj": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
}
]
}
]
}
}
And that would validate against either
"message": {
"another": "blah",
"#type": "foo",
"plainolstring": "sdfSR345w34"
}
or
"message": {
"another": "blah",
"#type": "foo",
"obj": {
"id":"sdfSR345w34",
"type": "guid"
}
}
Validating here against Schema v7 says that it is valid, but while it may be syntactically correct, would it achieve what I want?
Yes, anyOf will do what you want - except you have a syntax error by wrapping the subschemas with those properties "plainolstring" and "obj". The correct form would be:
{
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
]
}
Moreover, you don't even need the anyOf -- you can simplify that subschema to:
{
"type": ["string", "object"],
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
}
}
}
The properties keyword itself does not enforce that the type must be an object -- you need to use type for that. properties just says "if this is an object, this is the definition of those properties".

JSON Schema Nested If Then

I cannot seem to find a working way of applying multiple if/then logic on an enum.
anyOf doesnt apply the conditional logic, but instead it says if any of them match then thats good.
allOf again doesnt apply the conditional logic but tests a superset of the properties/required fields.
Here is a JSON Schema example:
{
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "http://example.com/root.json",
"type": "object",
"title": "The Root Schema",
"required": [
"type"
],
"properties": {
"type": {
"$id": "#/properties/type",
"enum": [
"a",
"b",
"c"
],
"title": "The Type"
},
"options": {
"$id": "#/properties/options",
"type": "object",
"title": "The Options Schema",
"oneOf": [
{
"if": { "properties": { "type": { "const": "a" } }
},
"then": {
"required": [ "option1" ],
"properties": {
"option1": {
"$id": "#/properties/options/properties/option1",
"type": "boolean",
"title": "The option1 Schema"
}
}
}
},
{
"if": { "properties": { "type": { "const": "b" } }
},
"then": {
"required": [ "option2" ],
"properties": {
"option2": {
"$id": "#/properties/options/properties/option2",
"type": "boolean",
"title": "The option2 Schema"
}
}
}
},
{
"if": { "properties": { "type": { "const": "c" } }
},
"then": {
"required": [],
"properties": {}
}
}
]
}
}
}
If you validate against this JSON:
{
"type": "a",
"options": {
"option1": true
}
}
It fails because option2 is required.
If you change it to anyOf then it succeeds, but if you change the JSON to be invalid:
{
"type": "a",
"options": {
"option2": false
}
}
It still succeeds.
I havent managed to get nested if/then/else/if/then/else working either.
How can i perform a check where I have set of properties for each type and you cannot intermingle them? Is this actually possible, or should I just change my design.
First, you can test your schemas here. There are several of these sites across the internet.
Second, the if/then/else construct was introduced to replace a oneOf for these kind of enum scenarios, not be combined with it.
This subschema
"if": { "properties": { "type": { "const": "a" } } },
"then": {
"required": [ "option1" ],
"properties": {
"option1": {
"$id": "#/properties/options/properties/option1",
"type": "boolean",
"title": "The option1 Schema"
}
}
}
doesn't actually fail when type is not a. It merely says that if type=a, apply the then subschema. It doesn't say anything about what to validate if type is not a, so it passes. If you add an else:false to this, it'll be more in line with what you're thinking, but I encourage you to think about it differently.
Use oneOf or if/then/else, but not both. I suggest changing your subschemas to use this format:
{
"properties": {
"type": { "const": "a" },
"option1": {
"$id": "#/properties/options/properties/option1",
"type": "boolean",
"title": "The option1 Schema"
}
},
"required": [ "option1" ],
}
This asserts that option1 is required and must be a boolean, and that type=a. If type is not a, this schema fails, which is what you want.
This answer describes what you need to do in a bit more detail.

JSON schema: Why does "constant" not validate the same way as a single-valued "enum"?

I have an object that provides a sort of audit log of versions of an asset. A couple of its properties (versionSource.metadata and versionSource.files) are objects that should validate against one of two schemas, depending on the value of one of their properties. I started off using a constant in my sub-schemas (inside the oneOf, but that was saying that all the the sub-schemas validated (thus breaking the oneOf since more than one validated. Changing it to a single-valued enum worked, though.
Why the difference in validation?
Here's the original schema:
{
"$id": "https://example.com/schemas/asset-version.json",
"title": "Audit log of asset versions",
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"required": [
"assetID",
"version",
"versionSource"
],
"properties": {
"assetID": {
"type": "string"
},
"version": {
"type": "integer",
"minimum": 1
},
"versionSource": {
"type": "object",
"properties": {
"metadata": {
"type": "object",
"oneOf": [
{
"properties": {
"sourceType": { "constant": "client" }
}
},
{
"$ref": "#/definitions/version-source-previous-version"
}
]
},
"files": {
"type": "object",
"oneOf": [
{
"properties": {
"sourceType": { "constant": "upload" },
"sourceID": {
"type": "string"
}
}
},
{
"$ref": "#/definitions/version-source-previous-version"
}
]
}
}
}
},
"definitions": {
"version-source-previous-version": {
"properties": {
"sourceType": { "constant": "previous-version" },
"sourceID": {
"type": "integer",
"minimum": 1
}
}
}
}
}
Here's one example document:
{
"assetID": "0150a186-068d-43e7-bb8b-0a389b572379",
"version": 1,
"versionSource": {
"metadata": {
"sourceType": "client"
},
"files": {
"sourceType": "upload",
"sourceID": "54ae67b0-3e42-464a-a93f-3143b0f078fc"
}
},
"created": "2018-09-01T00:00:00.00Z",
"lastModified": "2018-09-02T12:10:00.00Z",
"deleted": "2018-09-02T12:10:00.00Z"
}
And one more:
{
"assetID": "0150a186-068d-43e7-bb8b-0a389b572379",
"version": 2,
"versionSource": {
"metadata": {
"sourceType": "previous-version",
"sourceID": 1
},
"files": {
"sourceType": "previous-version",
"sourceID": 1
}
},
"created": "2018-09-01T00:00:00.00Z",
"lastModified": "2018-09-02T12:10:00.00Z",
"deleted": "2018-09-02T12:10:00.00Z"
}
Here's the error I get:
Message: JSON is valid against more than one schema from 'oneOf'. Valid schema indexes: 0, 1.
Schema path:
https://example.com/schemas/asset-version.json#/properties/versionSource/properties/metadata/oneOf
Since sourceType is a constant in both schemas inside the oneOf, I'm really not sure how my object could possibly be valid against both schemas.
Changing the schema to the following, though, worked:
{
"$id": "https://example.com/schemas/asset-version.json",
"title": "Audit log of asset versions",
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"required": [
"assetID",
"version",
"versionSource"
],
"properties": {
"assetID": {
"type": "string"
},
"version": {
"type": "integer",
"minimum": 1
},
"versionSource": {
"type": "object",
"properties": {
"metadata": {
"type": "object",
"oneOf": [
{
"properties": {
"sourceType": { "enum": [ "client" ] }
}
},
{
"$ref": "#/definitions/version-source-previous-version"
}
]
},
"files": {
"type": "object",
"oneOf": [
{
"properties": {
"sourceType": { "enum": [ "upload" ] },
"sourceID": {
"type": "string"
}
}
},
{
"$ref": "#/definitions/version-source-previous-version"
}
]
}
}
}
},
"definitions": {
"version-source-previous-version": {
"properties": {
"sourceType": { "enum": [ "previous-version" ] },
"sourceID": {
"type": "integer",
"minimum": 1
}
}
}
}
}
What am I missing?
It was my own typo ... constant should have been const. :facepalm:
according to draft 7
It should be noted that const is merely syntactic sugar for an enum with a single element, therefore the following are equivalent:
{ "const": "United States of America" }
{ "enum": [ "United States of America" ] }
some might find useful providing the default key when used in some render form solutions to have that single choice picked.
Hmm.. nothing is jumping out at me as incorrect. Since you're using draft-07 you could try writing it with if/then/else and see if the error is more helpful.
But...
Are you certain that the implementation you are using understands draft-07? If it ignored the $schema and ran it through draft-04 rules, it would not understand const. You should to check your tool documentation for this.

Create JSON Schema value dependency

I have a JSON schema I want to change by a dependency of one of the values of the JSON structure. For example if {"foo":1} also include {"fooBar":"number"} in the schema, resulting in {"foo":"number", "fooBar":"number"} but if {"foo":2} instead include {"fooBar2":"bool", "fooBar3":"string"} resulting in {"foo":1, "fooBar2":"bool", "fooBar3":"string"}. Is this possible.
I know how to make the inclusion of a key change the schema (code example from here) but I cannot find any example on how this could be done using values. If this is even possible.
{
"type": "object",
"properties": {
"name": { "type": "string" },
"credit_card": { "type": "number" },
"billing_address": { "type": "string" }
},
"required": ["name"],
"dependencies": {
"credit_card": ["billing_address"]
}
}
It can be done, but it's a little complicated. Below is the general pattern.
{
"type": "object",
"anyOf": [
{
"properties": {
"foo": { "enum": [1] },
"fooBar": { "type": "number" }
}
},
{
"properties": {
"foo": { "enum": [2] },
"fooBar2": { "type": "boolean" },
"fooBar3": { "type": "string" }
}
}
]
}