oneOf nested in array - jsonschema

I am currently testing JSON Forms on my schema (as is, without a UI definition), and I am running into the following problem: I can generate lists of options with oneOf (as shown in this_works below), but I am not able to do so within an array (this_does_not_work in the minimal example below). Instead, the array will simply offer simple text fields that validate against the options listed in oneOf.
Is there a way to achieve what I am trying to achieve here? Ideally, the UI of this_works (a list that users can select from) would appear for each line of the array:
{
"$id": "schema_test",
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Minimal example",
"type": "object",
"properties": {
"this_works": {
"type": "string",
"oneOf": [
{
"const": "a",
"title": "Option A"
},
{
"const": "b",
"title": "Option B"
}
]
},
"this_does_not_work": {
"type": "array",
"items": {
"type": "string",
"oneOf": [
{
"const": "a",
"title": "Option A"
},
{
"const": "b",
"title": "Option B"
}
]
}
}
}
}```

Related

JSON Schema array tuple validation with optional items

I'm writing a JSON schema capable of validating an array where each item has a different schema and the ordinal index of each item is meaningful but some items are optional.
However, using the current spec (2020-12) I can't use prefixItems with optional items.
To be clear:
all required items should exist and should be validated against index matching schema
missing optional items shouldn't invalidate the result
existing optional items should be validated against the index matching schema
Here is an example of the data I'm trying to validate:
(without optional array elements)
[
{
"name": "Document 1 required",
"url": "random.random/12313213.pdf"
},
{
"name": "Document 2 required",
"url": "random.random/12313213.pdf"
}
]
(with optional array elements)
[
{
"name": "Document 1 required",
"url": "random.random/1231322313.pdf"
},
{
"name": "Optional document 1",
"url": "random.random/1231356213.pdf"
},
{
"name": "Document 2 required",
"url": "random.random/1231893213.pdf"
},
{
"name": "Optional document 2",
"url": "random.random/1231336213.pdf"
}
]
Here is the current schema I'm using:
{
"type": "array",
"items": false,
"prefixItems": [
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "Document 1 required"
},
"url": {
"type": "string",
"format": "uri"
}
}
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "Document 2 required"
},
"url": {
"type": "string",
"format": "uri"
}
}
}
]
}
I've tried adding a oneOf in the optional items position with the correct schema and a stub {} but it doesn't seem to work as:
{
"type": "array",
"items": false,
"prefixItems": [
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "Document 1 required"
},
"url": {
"type": "string",
"format": "uri"
}
}
},
{
"oneOf": [
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "Optional document 1"
},
"url": {
"type": "string",
"format": "uri"
}
}
},
{}
]
},
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "Document 2 required"
},
"url": {
"type": "string",
"format": "uri"
}
}
},
{
"oneOf": [
{
"type": "object",
"properties": {
"name": {
"type": "string",
"const": "Optional document 2"
},
"url": {
"type": "string",
"format": "uri"
}
}
},
{}
]
}
]
}
Also tried different approaches using contains and additionalItems. However they either don't work for multiple schemas or don't guarantee the order for the optional items.
Note: the example uses similar schemas that could be simplified but it is used to showcase the issue in question.
EDIT:
As pointed out by #Relequestual the issue is that I'm trying to mix tuple validation with list validation where the data has an arbitrary length (required + optional) with a specific schema for each item.
This is not possible to achieve with the current version of the JSON Schema specification.
Your schema isn't working out because the schema {} is always true -- therefore when you say "oneOf": [ { .. some schema .. }, {} ] you are essentially negating the first schema - because the second schema must always be true, the first schema must be false. Which is the opposite of what you want!
I think you're expecting prefixItems to be more complicated than it actually is. Each schema in the prefixItems list is already optional, in the sense that if the corresponding item is not there in the data instance, there is no failure.
For example, consider validating this data [1] against this schema:
{
"prefixItems": [
{ "type": "integer" },
false
]
}
The overall result of this evaluation is true -- the first data element validates against "type": integer, and the second schema, false, never runs because there is no item to run against. If we passed a data instance of [ 1, 1 ] then validation would fail.
If you want to ensure that all of the data items corresponding to prefixItems subschemas are actually present, then you would need to use minItems: e.g. for the above example you would add "minItems": 2.
One major caveat is that you need to put all of your required items first. You cannot interleave optional items in between required items, as the schemas in prefixItems are always applied in order, and if one of the schemas doesn't evaluate to true, there is no "skipping" of items to the next one. The first prefixItems schema always applies to the first data instance, the second prefixItems schema always applies to the second data instance, and so on.
On the other hand, if you can get away with not specifying order at all, you can use multiple contains directives (note that minContains defaults to 1 when not explicitly provided):
"allOf": [
{ "contains": { schema for one of the required items, that can be anywhere... },
{ "contains": { schema for another required item... },
{ "minContains": 0, "contains": { schema for an optional item... },
...
]
You could also put your optional items into additionalItems with anyOf (this will work even if the number of optional items is a very large or unpredictable number):
"additionalItems": {
"anyOf": [
{ ..schema of an optional item.. },
{ "" },
...
]
}
If you can move all of your mandatory non-optional items to the front of the array, you can use prefixItems to define the required items, in order, followed by additionalItems to define a single schema for all other (optional) items, assuming the additional options are uniform.
Use minItems to make sure the number of required items are present. You can use maxItems to limit the total number of items in the array, effectivly allowing you to limit the number of optional items, if you need to do so.

How do I set additionalProperties to false when using an array?

The file being validated looks like this:
MyArray:
- someItemWithRandomName:
one: f9jfw9j302
two: 09dj0293jff
three: 09dj0293jff
- someOtherItemWithRandomName:
one: f9jfw9j302
two: 09dj0293jff
three: 09dj0293jff
- anotherItem:
one: f9jfw9j302
two: 09dj0293jff
three: 09dj0293jff
I'm validating it like this:
"MyArray": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"one",
"two",
"three"
],
"properties": {
"one": {
"type": "string"
},
"two": {
"type": "string"
},
"three": {
"type": "string"
}
}
}
I don't want to allow fields in the array items not defined in the schema but "additionalProperties": false doesn't work because the array item's keys can be any string. How do you accommodate this?
Edit
Here is a live example of my validation. The YAML I assume is going to be converted to JSON like in this example before it's validated: https://www.jsonschemavalidator.net/s/PBmLkkBl
You're missing a level in your schema, to allow the array item's object properties to be named anything.
Try this:
"MyArray": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"required": [
"one",
"two",
"three"
],
"properties": {
"one": {
"type": "string"
},
"two": {
"type": "string"
},
"three": {
"type": "string"
}
}
}
}
If you want to put a restriction on the names used in that intermediary level, you can replace "additionalProperties" with "patternProperties":
...
"patternProperties": {
"^[0-9]$": {
...
}
}
Or if you want to use a schema for the property names, you can use "propertyNames".
https://json-schema.org/understanding-json-schema/reference/object.html
(Posted solution on behalf of the question author to move it to the answer space).
My solution was to just remove the null items of the array because they were not being used. I think the only way to handle this correctly would be to do some kind of conditional validation to allow all properties with null values - if that's even possible.

Is it possible to be agnostic on the properties' names?

Let's say I want to have a schema for characters from a superhero comics. I want the schema to validate json objects like this one:
{
"Name": "Roberta",
"Age": 15,
"Abilities": {
"Super_Strength": {
"Cost": 10,
"Effect": "+5 to Strength"
}
}
}
My idea is to do it like that:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "characters_schema.json",
"title": "Characters",
"description": "One of the characters for my game",
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Age": {
"type": "integer"
},
"Abilities": {
"description": "what the character can do",
"type": "object"
}
},
"required": ["Name", "Age"]
}
And use a second schema for abilities:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "abilities_schema.json",
"title": "Abilities",
"type": "object",
"properties": {
"Cost": {
"description": "how much mana the ability costs",
"type": "integer"
},
"Effect": {
"type": "string"
}
}
}
But I can't figure how to merge Abilities in Characters. I could easily tweak the schema so that it validates characters formatted like:
{
"Name": "Roberta",
"Age": 15,
"Abilities": [
{
"Name": "Super_Strength"
"Cost": 10,
"Effect": "+5 to Strength"
}
]
}
But as I need the name of the ability to be used as a key I don't know what to do.
You need to use the additionalProperties keyword.
The behavior of this keyword depends on the presence and annotation
results of "properties" and "patternProperties" within the same schema
object. Validation with "additionalProperties" applies only to the
child values of instance names that do not appear in the annotation
results of either "properties" or "patternProperties".
https://json-schema.org/draft/2020-12/json-schema-core.html#rfc.section.10.3.2.3
In laymans terms, if you don't define properties or patternProperties the schema value of additionalProperties is applied to all values in the object at that instance location.
Often additionalProperties is only given a true or false value, but rememeber, booleans are valid schema values.
If you have constraints on the keys for the object, you may wish to use patternPoperties followed by additionalProperties: false.

reference multiple types in json schema array

I am trying to define an array property of an object in a json schema v7, but validation isn't working. How can I correctly reference multiple type definitions to be used in an array? Here an array for Directory can contain more directories or routes:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "RootDirectory",
"title": "Directory",
"description": "Build a directory for javascript app routing.",
"type": "object",
"definitions": {
"Route": {
"type": "object",
"description": "A simple endpoint declaration.",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"description": "An endpoint without forward slashes.",
"examples": ["welcome"],
"minLength": 1
},
"variableSuffix": {
"description": "A string that is appended to the path variable name during build.",
"type": "string"
}
},
"required": ["path"]
},
"Directory": {
"type": "object",
"description": "Contains child directories or routes for recursive tree building.",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"description": "An endpoint without forward slashes.",
"examples": ["welcome"],
"minLength": 1
},
"variableSuffix": {
"description": "A string that is appended to the path variable name during build.",
"type": "string"
},
"childNodes": {
"minItems": 1,
"type": "array",
"description": "An array of routes, linkedRoutes, subDirectories, or linkedSubDirectories.",
"contains": {
"type": "object",
"oneOf": [
{"$ref": "#/definitions/Route"},
{"$ref": "#/definitions/Directory"}
]
}
}
},
"required": ["path", "childNodes"]
},
},
"properties": {
"directories":{
"type": "array",
"contains": {
"type": "object",
"allOf": [{"$ref": "#/definitions/Directory"}]
},
"minItems": 1
},
"rootPath": {
"type": "string",
"default": ""
}
},
"required": ["directories"]
}
for instance, this is valid and shouldn't be since it contains an object with invalid properties:
{
"rootPath": "api",
"directories": [
{
"path": "a",
"childNodes": [
{
"path": "a",
"variableSuffix": "s"
},
{
"invalidProperty" : "a"
}
]
}
]
}
Is the problem that the property /directories/0/childNodes/1/invalidProperty should not be permitted? The schema does not state that all the array items in childNodes must be valid -- it only specifies that childNodes must contain a valid item, and item #0 does validate, therefore the overall schema validates.
To assert that all the items in childNodes must be valid, change contains at /definitions/Directory/properties/childNodes/contains to items -- the items keyword specifies a schema that must be validated against all array items, whereas contains only asserts that at least one array item must validate.

How do I indicate which "oneOf" API response will use?

I have an API where the basic response of one key will have an array of identifiers. A user may pass an extra parameter so the array will turn to an array of objects from an array of strings (for actual details rather than having to make a separate call).
"children": {
"type": "array",
"items": {
"oneOf": [{
"type": "string",
"description": "Identifier of child"
}, {
"type": "object",
"description": "Contains details about the child"
}]
}
},
Is there a way to indicate that the first type comes by a default and the second via a requested param?
It's not entirely clear to me what you are trying to accomplish with the distinction. Really that sounds like documentation; maybe elaborate in the descriptions of each oneOf subschema.
You could add an additional boolean field at the top level (sibling of children) to indicate whether detailed responses are returned and provide a default value for that field. The next step is to couple the value of the boolean to the type of the array items, which I've done using oneOf.
I'm suggesting something along the lines of:
{
"children": {
"type": "array",
"items": {
"oneOf": [
{
"type": "string",
"description": "Identifier of child",
"pattern": "^([A-Z0-9]-?){4}$"
},
{
"type": "object",
"description": "Contains details about the child",
"properties": {
"age": {
"type": "number"
}
}
}
]
}
},
"detailed": {
"type": "boolean",
"description": "If true, children array contains extra details.",
"default": false
},
"oneOf": [
{
"detailed": {
"enum": [
true
]
},
"children": {
"type": "array",
"items": {
"type": "object"
}
}
},
{
"detailed": {
"enum": [
false
]
},
"children": {
"type": "array",
"items": {
"type": "string"
}
}
}
]
}
The second oneOf places a further requirement on the response object that when "detailed": true the type of items of the "children" array must be "object". This refines the first oneOf restriction that describes the schema of objects in the "children" array.