I am trying to make a schema for validating a policy language. Short:
a policy is associated with an assertion. This assertion can either be an operator (and, or, not) and contains a list of other assertions. An assertion can also be a primitive (leaf node).
I made an UML design to make things easier to understand:
UML diagram
{
"policy": {
"name": "test",
"expression": {
"operator": "all",
"value": [
{
"primitive": "encrypt",
"preference": 12345,
"usage": "required"
},
{
"primitive": "sign",
"preference": 12345,
"usage": "required"
}
],
"preference": 12345,
"usage": "required"
}
}
}
And here is the scheme I currenty made:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"minProperties": 1,
"additionalProperties": {
"$ref": "#/definitions/policy"
},
"definitions": {
"policy": {
"title": "Policy",
"type": "object",
"required": [
"name",
"expression"
],
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"expression": {
"$ref": "#/definitions/assertion"
}
}
},
"operator": {
"properties": {
"value": {
"type": "array",
"items": {
"$ref": "#/definitions/assertion"
}
},
"operator": {
"enum": [
"allOne",
"all"
]
}
}
},
"primitive": {
"properties": {
"primitive": {
"enum": [
"encrypt",
"sign"
]
}
}
},
"assertion": {
"type": "object",
"additionalProperties": false,
"oneOf": [
{
"$ref": "#/definitions/operator"
},
{
"$ref": "#/definitions/primitive"
}
],
"properties": {
"preference": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true
},
"usage": {
"enum": [
"required",
"rejected",
"optional",
"observed",
"ignored"
]
}
}
}
}
}
With the use of "oneOf" I'm trying to either use the specification of an operator or a primitive. But I'm not sure this is the way to go because i received the following error:
"message" : "object instance has properties which are not allowed by the schema: [\"operator\",\"value\"]"
The error message complains about extra properties which are not defined (because of "additionalProperties": false). However, these are defined in the definitions...
The oneOf, anyOf, ... keywords cannot be used for referencing other definitions. They do work for required. The solution is to declare all properties and only require the properties that are needed in the context. My example would become the following:
"assertion": {
"type": "object",
"additionalProperties": false,
"oneOf": [
{
"required": [
"operator"
]
},
{
"required": [
"primitive"
]
}
],
"properties": {
"operator": {
"$ref": "#/definitions/operator"
},
"primitive": {
"$ref": "#/definitions/primitive"
},
"preference": {
"type": "integer",
"minimum": 0,
"exclusiveMinimum": true
},
"usage": {
"enum": [
"required",
"rejected",
"optional",
"observed",
"ignored"
]
}
}
}
Related
I am attempting to validate the following JSON file:
{
"Transaction": {
"Header": {
"Workflow": "Rejection",
"Job-Offer": {
"Offer-Status": "New",
"Datetime-Offered": "2017-12-15T16:00:00",
"Accepted": "YES",
"Datetime-Accepted": "2017-12-15T16:00:00"
}
}
}
}
against the following schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Schema",
"description": "Schema",
"$ref": "#/defs/Schema",
"defs": {
"Schema": {
"type": "object",
"additionalProperties": false,
"properties": {
"Transaction": {
"$ref": "#/defs/Transaction"
}
},
"required": [
"Transaction"
],
"title": "Schema"
},
"Transaction": {
"type": "object",
"additionalProperties": false,
"properties": {
"Transaction-Header": {
"$ref": "#/defs/Transaction-Header"
}
},
"required": [
"Transaction-Header"
],
"title": "Transaction"
},
"Transaction-Header": {
"type": "object",
"additionalProperties": false,
"properties": {
"Workflow": {
"type": "string",
"enum": [
"Offer",
"Transfer",
"Acceptance",
"Rejection",
"Cancellation",
"Update"
]
},
"Job-Offer": {
"$ref": "#/defs/JobOffer"
}
},
"required": [
"Workflow"
],
"title": "Transaction-Header"
},
"JobOffer": {
"description": "Job Offer.",
"type": "object",
"additionalProperties": true,
"properties": {
"Offer-Status": {
"type": "string",
"enum": [
"New",
""
]
},
"Datetime-Offered": {
"type": "string",
"format": "date-time"
},
"Accepted": {
"type": "string",
"enum": [
"YES",
"NO",
""
]
},
"Datetime-Accepted": {
"type": "string",
"format": "date-time"
},
"Reason-Rejected": {
"type": "string",
"minLength": 0,
"maxLength": 30
},
"Offer-Cancelled": {
"type": "string",
"enum": [
"YES",
"NO",
""
]
},
"Datetime-Cancelled": {
"type": "string",
"format": "date-time"
}
},
"allOf": [
{ "$ref": "#/defs/JOBACCEPT" },
{ "$ref": "#/defs/JOBREJECT" }
],
"required": [
"Offer-Status"
],
"title": "JobOffer"
},
"JOBACCEPT": {
"properties": {
"Workflow": { "enum": [ "Acceptance" ] }
},
"required": [
"Accepted",
"Datetime-Accepted"
],
},
"JOBREJECT": {
"properties": {
"Workflow": { "enum": [ "Rejection" ] }
},
"required": [
"Reason-Rejected"
],
}
}
}
What I am after is:
If the Workflow of "Acceptance" is selected, the fields under JOBACCEPT are required.
If the Workflow of "Rejection" is selected, the fields under JOBREJECT are required.
I have tried many different combinations of oneOf, allOf, anyOf, if-then-else but nothing seems to work correctly.
Anyone have any ideas what needs to be done?
Re-worked json inline:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"type": "object",
"properties": {
"Transaction": {
"type": "object",
"properties": {
"Transaction-Header": {
"type": "object",
"properties": {
"Workflow": {
"type": "string",
"enum": [
"Offer",
"Transfer",
"Acceptance",
"Rejection",
"Cancellation",
"Update"
]
},
"Job-Offer": {
"type": "object",
"properties": {
"Offer-Status": {
"type": "string",
"enum": [
"New",
""
]
},
"Datetime-Offered": {
"type": "string",
"format": "date-time"
},
"Accepted": {
"type": "string",
"enum": [
"YES",
"NO",
""
]
},
"Datetime-Accepted": {
"type": "string",
"format": "date-time"
},
"Reason-Rejected": {
"type": "string",
"minLength": 0,
"maxLength": 30
},
"Offer-Cancelled": {
"type": "string",
"enum": [
"YES",
"NO",
""
]
},
"Datetime-Cancelled": {
"type": "string",
"format": "date-time"
}
},
"required": [
"Offer-Status"
]
},
"readOnly": true
},
"required": [
"Workflow"
]
}
},
"required": [
"Transaction-Header"
]
}
},
"allOf": [
{
"if": {
"properties": {
"Transaction": {
"properties": {
"Transaction-Header": {
"properties": {
"Workflow": {
"enum": [
"Acceptance"
]
}
},
"required": [
"Workflow"
]
}
}
}
}
},
"then": {
"properties": {
"Transaction": {
"properties": {
"Transaction-Header": {
"properties": {
"Job-Offer": {
"properties": {},
"required": [
"Accepted",
"Datetime-Accepted"
]
}
}
}
}
}
}
}
}
],
"required": [
"Transaction"
]}
You had the right idea. The problem is where you've placed the allOf with your conditionals. You have it in the "JobOffer" schema, but are trying to set constraints on the "Workflow" property which is in the "Transaction-Header" schema. There is no way to reference a property that is higher up in the JSON tree structure, so you need to move the allOf up into the "Transaction-Header" schema so you can set constraints on the "Workflow" property.
Now that it's in the right place, the best way to express your conditional constraints is with if/then. The context of the if/then is now the the "Transaction-Header" schema, so the then schemas needs to not just say what properties are required, they need to declare that those properties are in the "Job-Offer" object.
{
...
"defs": {
...
"Transaction-Header": {
...
"allOf": [
{ "$ref": "#/defs/JOBACCEPT" },
{ "$ref": "#/defs/JOBREJECT" }
]
},
"JOBACCEPT": {
"if": {
"type": "object",
"properties": {
"Workflow": { "enum": ["Acceptance"] }
},
"required": ["Workflow"]
},
"then": {
"properties": {
"Job-Offer": {
"required": ["Accepted", "Datetime-Accepted"]
}
}
}
},
"JOBREJECT": { ... Similar to JOBACCEPT ... }
}
}
You're abstracting everything away into definitions *, so it's tricky to express conditionals that reference things multiple layers deep. If you inline all the definitions, it gets a little easier to see what needs to be done.
The if/then keywords need to be at the level of 'Transaction'. In pseudocode:
"if the property 'Header' exists (i.e. required) and its value is ... (const), then require property ... with value (type=object, required properties=[...], property definitions=...)", and so on.
* by the way, in version 2020-12 the definitions keyword is $defs -- it may work the way you have it, but implementations will be unable to validate the schemas undef defs as they won't recognize them there, so some errors can slip through and be harder to find.
I have the following JSON data
[
{
"type": "social_media_profiles",
"data": {
"profiles": [
{
"key": "twitter",
"data": "username",
"field": "handle",
"label": "Tweet"
},
{
"key": "customLink",
"data": "abc",
"field": "url",
"label": "Click",
"color": {
"button": "red",
"text": "green",
"border": "yellow"
}
}
]
}
}
]
and following jsong schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"additionalProperties": false,
"items": {
"type": "object",
"required": ["type", "data"],
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"enum": [
"social_media_profiles"
]
},
"data": {
"type": "object"
}
},
"allOf": [
{
"if": {
"properties": {
"type": {
"const": "social_media_profiles"
}
}
},
"then": {
"properties": {
"data": {
"type": "object",
"required": ["profiles"],
"additionalProperties": false,
"properties": {
"profiles": {
"type": "array",
"items": {
"type": "object",
"required": ["data", "field", "key"],
"additionalProperties": false,
"properties": {
"data": {
"type": "string",
"description": "Data contains either profile url, handle id, etc."
},
"field": {
"type": "string",
"enum": ["url", "handle", "id", "tel"],
"description": "Type of field value."
},
"key": {
"type": "string",
"description": "Social media name used to distinguish each social network"
},
"label": {
"type": "string",
"description": "Label to display on the landing page"
}
},
"allOf": [
{
"if": {
"properties": {
"key": {
"const": "customLink"
}
}
},
"then": {
"properties": {
"color": {
"type": "object",
"additionalProperties": false,
"required": [],
"properties": {
"button": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
}
}
}
}
}
}
]
}
}
}
}
}
}
}
]
}
}
I want to add new property color to the profiles item based on the condition when key of the item is customLink.
If key is not customLink then color property should not be there.
Validating the schema from https://www.jsonschemavalidator.net/ is giving error
Found 2 error(s)
Message: JSON does not match all schemas from 'allOf'. Invalid schema indexes: 0.
Schema path: #/items/allOf
Message: JSON does not match schema from 'then'.
Schema path: #/items/allOf/0/then/then
How can I append new property conditionally based on the sibling property value?
In your profiles.items schema, you defined additionalProperties: false.
additionalProperties depends on the properties defined in the same schema object, meaning that color would always be dissallowed.
You can separate out your concerns into "what properties are allowed", and "if their values are valid".
Moving the validation for the color object into profiles.properties allows you to maintain additionalProperties: false in that object level.
The schema values of the properties object are only applied to the instance location for the keys, if they exist (hence needing to use required to require that specific keys are... required).
Having simplified the condional section, you end up with a cleaner Schema.
You now only need to define the condition under which color is required, without worrying about what the value should look like.
Sudo code: if key is customLink, then require color, else dissallow color.
{
"if": {
"properties": {
"key": {
"const": "customLink"
}
}
},
"then": {
"required": [
"color"
]
},
"else": {
"not": {
"required": [
"color"
]
}
}
}
Using not, you can invert the validation result of the applied subschema, which is what you need to do when you want to define that a specific property is NOT allowed.
Here's a live demo to play with: https://jsonschema.dev/s/C9V6N
And for prosperity, here's the full schema, with one or two redundancies removed.
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": {
"type": "object",
"required": [
"type",
"data"
],
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"enum": [
"social_media_profiles"
]
},
"data": {
"type": "object"
}
},
"allOf": [
{
"properties": {
"data": {
"type": "object",
"required": [
"profiles"
],
"additionalProperties": false,
"properties": {
"profiles": {
"type": "array",
"items": {
"type": "object",
"required": [
"data",
"field",
"key"
],
"additionalProperties": false,
"properties": {
"data": {
"type": "string",
"description": "Data contains either profile url, handle id, etc."
},
"field": {
"type": "string",
"enum": [
"url",
"handle",
"id",
"tel"
],
"description": "Type of field value."
},
"key": {
"type": "string",
"description": "Social media name used to distinguish each social network"
},
"label": {
"type": "string",
"description": "Label to display on the landing page"
},
"color": {
"type": "object",
"additionalProperties": false,
"properties": {
"button": {
"type": "string"
},
"text": {
"type": "string"
},
"border": {
"type": "string"
}
}
}
},
"allOf": [
{
"if": {
"properties": {
"key": {
"const": "customLink"
}
}
},
"then": {
"required": [
"color"
]
},
"else": {
"not": {
"required": [
"color"
]
}
}
}
]
}
}
}
}
}
}
]
}
}
I must be missing something here, but the below JSON is not getting validated against the schema.
For example, the required attribute from the Java/JavaScript object is never getting enforced as per the schema. (FYI- Every language object may have other attributes or nested object)
However, if I completely remove the definition and directly put under array items each separately, then it validates.
I want to use 'definitions' and gets validated.The reason I have to put all the object in definitions and later I may have to put different language object in oneOf/allOf for other certain validation check.
Online check:
Schema and JSON
JSON
{
"languages": [
{
"lang": "Java",
"trainer": "Peter"
},
{
"lang": "JavaScript",
"enrolled": "42",
"available": "5"
}
]
}
and the Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"languages"
],
"properties": {
"languages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object"
},
"anyOf": [
{
"$ref": "#/definitions/Java"
},
{
"$ref": "#/definitions/JavaScript"
}
]
}
},
"definitions": {
"Java": {
"required": [
"trainer"
],
"properties": {
"lang": {
"enum": [
"Java"
]
},
"trainer": {
"type": "string"
}
}
},
"JavaScript": {
"required": [
"enrolled",
"available"
],
"properties": {
"lang": {
"enum": [
"JavaScript"
]
},
"enrolled": {
"type": "string"
},
"available": {
"type": "string"
}
}
}
}
}
The fixed schema and it works now
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [
"languages"
],
"properties": {
"languages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"anyOf": [
{
"$ref": "#/definitions/Java"
},
{
"$ref": "#/definitions/JavaScript"
}
]
}
}
},
"definitions": {
"Java": {
"required": [
"trainer"
],
"properties": {
"lang": {
"enum": [
"Java"
]
},
"trainer": {
"type": "string"
}
}
},
"JavaScript": {
"required": [
"enrolled",
"available"
],
"properties": {
"lang": {
"enum": [
"JavaScript"
]
},
"enrolled": {
"type": "string"
},
"available": {
"type": "string"
}
}
}
}
}
I think that the "array" definition is not correct. The objects that can be added in the array must be refereed in the "items", after you define the type of the items. Something like this:
"languages": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"anyOf": [
{"$ref": "#/definitions/Java"},
{"$ref": "#/definitions/JavaScript"}
]
}
}
I have fixed myself
What I did: I have placed the json object blocks under items section of array.
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.
I have the input json like below,
{"contents":[{"type":"field"},{"type":"field","itemId":"594b9980e52b5b0768afc4e8"}]}
the condition is,
if the type is 'field', then 'itemId' should be the required field
and if the type is 'fieldGroup' or 'subSection', then 'itemId' is optional
This is the Json Schema I tried and its not working as expected,
"type": "object",
"additionalProperties": false,
"properties" : {
"contents" : {
"type" : "array",
"items": {"$ref": "#displayItem" }
}
},
"definitions": {
"displayItem" : {
"id": "#displayItem",
"type": "object",
"items": {
"anyOf": [
{"$ref": "#fieldType"},
{"$ref": "#fieldGroupSubSectionType"}
]
}
},
"fieldType" : {
"id": "#fieldType",
"type": "object",
"additionalProperties": false,
"properties": {
"itemId": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["field"]
}
}
},
"fieldGroupSubSectionType" : {
"id": "#fieldGroupSubSectionType",
"type": "object",
"additionalProperties": false,
"properties": {
"itemId": {
"type": [ "string", "null" ]
},
"type": {
"type": "string",
"enum": [
"fieldGroup",
"subSection"
]
}
}
}
}
Any help / workaround with Sample Json Schema to achieve the above use case is appreciated.
If I understand the description of what you want correctly, then the json example you provide is not valid since it has a type: "field" but does not have an "itemId" property.
Assuming that is true. Instead of using
type: ["string", null]
use the required property.
I changed your schema a bit, instead of having separate definitions I inlined them, but other than that (and the use of required) is the same:
{
"type": "object",
"additionalProperties": false,
"properties": {
"contents": {
"type": "array",
"items": {
"anyOf": [
{
"type": "object",
"additionalProperties": false,
"properties": {
"itemId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"field"
]
}
},
"required": [
"itemId"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"itemId": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"fieldGroup",
"subSection"
]
}
}
}
]
}
}
}
}
Here is your answer with a little cleanup for best practices and style. The trick is that you need to use implication "a implies b <=> (not a) or b". In this case you have "type = field implies itemId is required <=> type is not field or itemId is required".
{
"type": "object",
"properties": {
"contents": {
"type": "array",
"items": { "$ref": "#/definitions/displayItem" }
}
},
"definitions": {
"displayItem": {
"type": "object",
"properties": {
"itemId": { "type": "string" },
"type": { "enum": ["field", "fieldGroup", "subSection"] }
},
"anyOf": [
{ "not": { "$ref": "#/definitions/fieldType" } },
{ "required": ["itemId"] }
]
},
"fieldType": {
"properties": {
"type": { "enum": ["field"] }
}
}
}
}