Make a property required depending on a query parameter with OpenAPI and JSON-Schema - jsonschema

I have a REST API that support for most calls a with query parameter, allowing the user to specify which additional fields they want to load along with the resource’s basic data.
I can describe the query parameter with OpenAPI, and I can describe the format of the response with JSON-Schema. But how can I describe the relation between the parameter and the response?
What I want to describe is:
/api/resource/{id} returns:
{"id": 123, … }
/api/resource/{id}?with=stuff returns:
{"id": 123, …, "stuff": { … }}
I would like this stuff property to be defined as required. Is there a way of doing this?
(What makes this probably even more impossible is that the value of with can be a comma-separated list of properties, but at this point I’m not hoping)

You can't do anything in JSON Schema using out of context data. So, the only way to do what you want is to put the query data into your response.
{
"id": 123,
"query": {},
...
}
{
"id": 123,
"with": ["stuff"],
...
"stuff": { ... }
}
With pure JSON Schema or OpenAPI 3.1, you can use if/then and contains to express the conditional requirement.
{
"type": "object",
"properties": {
"id": { "type": "object" },
"with": { "type": "array", "items": { "type": "string" } },
...
"stuff": { ... }
},
"required": ["id"],
"allOf": [
{
"if": {
"properties": {
"with": { "contains": { "const": "stuff" } }
},
"required": ["with"]
},
"then": { "required": ["stuff"] }
},
... additional conditionals ...
]
}
However, assuming you are on OpenAPI 3.0, you don't have if, then, or contains. It's still possible, but it gets a bit ugly because you need to use a bunch of confusing boolean logic concepts instead.
{
"type": "object",
"properties": {
"id": { "type": "object" },
"with": { "type": "array", "items": { "type": "string" } },
...
"stuff": { ... }
},
"required": ["id"],
"allOf": [
{
"anyOf": [
{
"not": {
"properties": {
"with": {
"not": {
"items": { "not": { "enum": ["stuff"] } }
}
}
},
"required": ["with"]
}
},
{ "required": ["stuff"] }
]
},
... additional conditionals ...
]
}

JSON schema: query string required
schema: {
tags: ["sample"],
summary: "Set query string values to required",
querystring: {
type: "object",
properties: {
name: {
type: "string",
},
address: {
type: "string",
},
},
required: ["name"],
},
},

Related

How to improve JSON Schema validation results when using "anyOf"

We want to validate an array of elements with a JSON Schema Validator (https://github.com/networknt/json-schema-validator if that makes any difference).
Unfortunately we get quite ambiguous error messages if the validation fails and I am wondering if there is a way to improve this.
To make it clearer, I have created a small sample:
Each element in the array has a property "pet_type" which we can be sure to be there all the time (yeah I know that is discussable, but for the sake of argument let's ignore that for now). Each element has then some other properties.
This is the json schema right now.
Envelope (Basically with an array of elements)
{
"title": "Envelope",
"type": "object",
"properties": {
"pets": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "./Cat.json"
},
{
"$ref": "./Dog.json"
}
]
}
}
}
}
Cat (one of the elements in the array)
{
"title": "Cat",
"allOf": [
{
"$ref": "Pet.json"
},
{
"type": "object",
"properties": {
"hunts": {
"type": "boolean"
},
"age": {
"type": "integer"
},
"pet_type": {
"type": "string",
"enum": [
"Cat"
],
"pattern": "Cat"
}
},
"required": [
"pet_type",
"age"
]
}
]
}
Dog:
{
"title": "Dog",
"allOf": [
{
"$ref": "Pet.json"
},
{
"type": "object",
"properties": {
"bark": {
"type": "boolean"
},
"breed": {
"type": "string"
},
"pet_type": {
"type": "string",
"enum": [
"Dog"
],
"pattern": "Dog"
}
},
"required": [
"bark"
]
}
]
}
And the Pet.json, which specifies that "pet_type" is required on each of the elements.
{
"title": "Pet",
"type": "object",
"discriminator": {
"propertyName": "pet_type"
},
"properties": {
"pet_type": {
"type": "string",
"enum": [
"Cat",
"Dog"
]
}
},
"required": [
"pet_type"
]
}
The problem I have is that the error messages get very long, because the element is validated against all of the constraints, without narrowing it down a bit.
Let's say the JSON to validate looks like this:
{
"pets": [
{
"pet_type": "Cat",
"hunts": true
}
]
}
"pet_type" is set to "Cat", so for a human it is "clear" that it should only validate against the Cat JSON Schema and then show that "age" is missing.
What really happens is that there are 4 violations:
age is missing (from the Cat JSON Schema)
bark is missing (from the Dog JSON Schema)
pet_type is violating the "Dog" enum contstraint (from the Dog JSON Schema)
pet_type is violating the "Dog" regex pattern (from the Dog JSON Schema)
Note: I have added the enum/regex pattern in an act of trying to change the behavior, but it did not work.
I understand from a technical standpoint why it behaves like it does, I just want to know if it is possible to tell the validator somehow to first narrow down the validation based on pet_type and then keep validating?
What I tried to get it working:
I have tried to set the pet_type to string with an enum "Cat" and "Dog" and added one of those values to the Cat/Dog JSON Schema respectively in an attempt to make it clear which event has which pet_type.
I have tried to remove the Pet.json Schema completely, but that did not change anything.
I have tried to use if/then, but somehow this gets rid of all validation errors:
{
"title": "Envelope",
"type": "object",
"properties": {
"pets": {
"type": "array",
"items": {
"anyOf": [
{
"if": {
"properties": {
"pet_type": {
"const": "Cat"
}
}
},
"then": {
"$ref": "./Cat.json"
}
},
{
"if": {
"properties": {
"pet_type": {
"const": "Dog"
}
}
},
"then": {
"$ref": "./Dog.json"
}
}
]
}
}
}
}
My sample I tried to validate gives no errors anymore:
{
"pets": [
{
"pet_type": "Cat",
"hunts": true
}
]
}
Turns out the solution with if/then was correct, and the json-schema-validator actually detects the problems, but has some code that should improve the error messages, but gets rid of the validation errors instead.
I will bring the problem to their attention.
For now I have found a workaround to use nested if/then/else statements:
{
"title": "Envelope",
"type": "object",
"properties": {
"pets": {
"type": "array",
"items": {
"if": {
"properties": {
"pet_type": {
"const": "Cat"
}
}
},
"then": {
"$ref": "./Cat.json"
},
"else": {
"if": {
"properties": {
"pet_type": {
"const": "Dog"
}
}
},
"then": {
"$ref": "./Dog.json"
},
"else": {
"properties": {
"pet_type": {
"type": "string",
"enum": [
"Cat",
"Dog"
]
}
}
}
}
}
}
},
"additionalProperties": false
}

requiring a param with JSON Schema when another param is not present

update: added requirements
I am trying to implement a JSON schema that works like so…
given the following params: foo, bar, baz, one, and two
One of the following should apply
either no param exists
if one or more params exist then
if foo exists
- then one and two are not required
else if any param(s) other than foo exist(s)
- then one and two are required
of course, if any params are provided that are not in the schema then an appropriate error should be raised.
After a lot of finagling, and inspiration, here is my attempt which doesn't really work
{
"type": "object",
"properties": {
"foo": { "type": "string" },
"bar": { "type": "string" },
"baz": { "type": "string" }
},
"oneOf": [
{
"not": {
"properties": { "foo": { "const": "" } },
"required": []
}
},
{ "required": [ "one", "two" ] }
]
}
I also tried the following but was not successful
{
"type": "object",
"properties": {
"foo": { "type": "string" },
"bar": { "type": "string" },
"baz": { "type": "string" }
},
"dependencies": {
"not": {
"foo": { "required": ["one", "two"] }
}
}
}
I suspect there might be a hidden requirement or two in here, but here's a solution based on your requirements. Let me know if there's something I'm missing and I'll update my answer.
Most of your requirements are the default behavior in JSON Schema, so you don't have to do anything.
either no param exists
All properties are optional by default, so nothing to do here
if foo exists - then one and two are not required
Again, optional is the default, so nothing to do here
if one or more params exist then [and] any param(s) other than foo exist(s) - then one and two are required
The best way to express this is with if/then. If "foo" is optional, it's a little complicated to express, "any param(s) other than foo", so I put that in definitions to make it a bit easier to read.
if any params are provided that are not in the schema then an appropriate error should be raised.
The additionalProperties keyword takes care of this. You just need to add "one" and "two".
{
"type": "object",
"properties": {
"foo": { "type": "string" },
"bar": { "type": "string" },
"baz": { "type": "string" },
"one": {},
"two": {}
},
"additionalProperties": false,
"if": { "$ref": "#/definitions/has-property-other-than-foo" },
"then": { "required": ["one", "two"] },
"definitions": {
"has-property-other-than-foo": {
"if": { "required": ["foo"] },
"then": { "minProperties": 2 },
"else": { "minProperties": 1 }
}
}
}

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

can I do a patternProperties with $ref validation

I am trying to match a patternProperties with a schema, like, here is the jsonschema text:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
"fabric_id": {
"enum": [
"ADMIN",
"COPPER",
"NETWORK",
"STORAGE",
"STORAGE2",
"TENANT"
]
},
"fabrics": {
"additionalProperties": false,
"patternProperties": {
"[A-Z0-9-]*": {
"additionalProperties": false,
"properties": {
"description": {
"type": "string"
},
"fabric_id": {
"$ref": "#/definitions/fabric_id",
"type": "string"
}
},
"required": [
"description",
"fabric_id"
],
"type": "object"
}
},
"type": "object"
}
},
"description": "fabrics spec",
"properties": {
"fabrics": {
"$ref": "#/definitions/fabrics"
}
},
"required": [
"fabrics"
],
"title": "network fabric",
"type": "object"
}
and here is my input json file:
{
"fabrics": {
"ADMIN": {
"description": "Primary bonded pair on the bigswitches.",
"fabric_id": "ADMIN"
},
"COPPER": {
"description": "Primary IPMI fabric on the tor switches.",
"fabric_id": "COPPER"
}
}
}
I can't figure out how to validate the patternProperty against the fabric_id enum? The pattern object has fabric_id in it, and that is able to reference the fabric_id enum in the definitions section. I'd like to have that same $ref for the "[A-z0-9-]*" pattern, but I just can't make it work. Is this possible?
This schema does is the best you can do. The only thing it can't do is constrain the property name to match the value of "fabric_id". Unfortunately, this is not possible with JSON Schema.
{
"$schema": "http://json-schema.org/draft-06/schema#",
"type": "object",
"properties": {
"fabrics": { "$ref": "#/definitions/fabrics" }
},
"required": ["fabrics"],
"definitions": {
"fabric_id": {
"enum": ["ADMIN", "COPPER", "NETWORK"]
},
"fabrics": {
"type": "object",
"propertyNames": { "$ref": "#/definitions/fabric_id" },
"patternProperties": {
".*": {
"type": "object",
"properties": {
"description": { "type": "string" },
"fabric_id": { "$ref": "#/definitions/fabric_id" }
},
"required": ["description", "fabric_id"]
}
}
}
}
}
Sadly, I don't believe this is possible with draft-4 of JSON Schema.
If you can upgrade to 6 or 7 (+), you can make this possible.
propertyNames: https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6.5.8
If the instance is an object, this keyword validates if every property name in the instance validates against the provided schema. Note the property name that the schema is testing will always be a string.
An example of how this can be used can be found at https://github.com/json-schema-org/json-schema-org.github.io/issues/77
...
"fooProperties": {
"propertyNames": {
"$comment": "Need to anyOf these or else the enum and pattern conflict",
"anyOf": [
{"enum": ["foo1", "foo2"]},
{"pattern": "foo[A-Z][a-z0-9]*"}
]
}
},
...
Sorry I don't have time to update your schema to follow this, but hopefully I sufficiently explained this for you to adapt it.
if you're unable to migrate beyond draft-4... well you'll have to do that validation aspect manually outside of JSON Schema.

Json Schema: Require a property only when a specific property is present in a deep nested object

I need to build a json schema (draft 4) that requires a property based on the presence of a property in another nested object. I already searched and tried a lot of things (anyOf, oneOf, not, dependencies) with no luck.
Maybe this is not possible to in json schema?
This is my simplified schema:
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": ["dog"],
"properties": {
"dog": {
"type": "object",
"required": ["bananas"],
"properties": {
"bananas": { "$ref": "bananas.json" },
"thing": {
"type": "object",
"properties": {
"total": { "type": "string" }
}
}
}
}
}
}
And this is bananas.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": ["banana"],
"definitions": {
"non-empty-string": {
"type": "string",
"minLength": 1
}
},
"properties": {
"banana": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["unit"],
"properties": {
"unit": { "type": "string" },
"thing": {
"type": "object",
"anyOf": [
{ "required": [ "tax_transfers" ] },
{ "required": [ "tax_retentions" ] }
],
"properties": {
"tax_transfers": {
"type": "object",
"required": ["tax_transfer"],
"properties": {
"tax_transfer": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"rate": { "type": "string" }
}
}
}
}
},
"tax_retentions": {
"type": "object",
"required": ["tax_retention"],
"properties": {
"tax_retention": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"rate": { "type": "string" }
}
}
}
}
}
}
}
}
}
}
}
}
I need that when one or more objects in the array have a 'thing' property (at bananas -> banana -> thing).
Then the property 'thing' at (dog -> thing) should be required.
Any help would be really appreciated.
You need two things to express your constraint. The first is "contains" and the other is "implication". I've organized each in the definitions section.
Contains
The items keyword allows us to require that all items in an array are valid against a schema. If it is not true that all of the items in the array are not valid against the schema, then we know that at least one item is valid.
{
"not": {
"items": { "not": { ... schema ... } }
}
}
If you are able to upgrade to JSON Schema draft-06, a contains keyword was added to make this much easier.
{
"contains": { ... schema ... }
}
Implication
Implication allows you to do something like a conditional. The condition schema implies the constraint schema if either the condition is true, or the constraint is true (or both are true). It's effectively the same as saying, if the condition is true then the constraint must also be true.
{
"anyOf": [
{ "not": { ... condition schema ... } },
{ ... constraint schema ... }
]
}
JSON Schema draft-07 adds the if-then-else keywords in attempt to address this case better. I personally dislike the way this was done enough that I'll stick with the implication pattern for this kind of thing, but here it is in case you want to try it.
{
"if": { ... schema ... },
"then": { ... schema ... },
"else": { ... schema ... }
}
All together
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": ["dog"],
"properties": {
"dog": {
"type": "object",
"required": ["bananas"],
"properties": {
"bananas": { "$ref": "bananas.json" },
"thing": { "type": "object" }
}
}
},
"allOf": [
{ "$ref": "#/definitions/banana-things-implies-dog-things" }
],
"definitions": {
"banana-has-things": {
"properties": {
"dog": {
"properties": {
"bananas": {
"properties": {
"banana": {
"not": {
"items": { "not": { "required": ["things"] } }
}
}
}
}
}
}
}
},
"banana-things-implies-dog-things": {
"anyOf": [
{ "not": { "$ref": "#/definitions/banana-has-things" }},
{
"properties": {
"dog": { "required": ["things"] }
}
}
]
}
}
}