jsonjsonschemajson-schema-validator

Restrict JSON array to only accept items of a specific type


I have a moderately complex JSON schema containing an array which shall only accept items of two specific types. For some reason, after adding an item of a third type, validation succeeds. Can you help me find what I overlook?

I've made a small fragment of the scheme with only the affected parts:

{
    "$schema": "http://json-schema.org/draft-07/schema#",    
       
    "type": "object",
    "additionalProperties": false,
    
    "properties": {

        "trainzMeshes": {
            "title": "Trainz Meshes",
            "description": "Specifications of Trainz meshes to create as FBX files and comments specified as an array.",
            "type": "array",      
            "additionalItems": false,
            "items": {
                "anyOf": [ 
                    { 
                        "comment": {
                            "$ref": "#/$defs/comment" 
                        },
                        "minItems": 0
                    }, 
                    {
                        "$ref": "#/$defs/trainzMesh",
                        "minItems": 1
                    } 
                ]
            }                          
        },
    },
        

    
    "$defs": {

        "comment": {
            "title": "Comment",
            "description": "A comment to help you remember what comes next. You may include as many comments as you wish.",
            "oneOf": [
                { 
                    "type": "string" 
                },
                { 
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "minItems": 1
                }
            ]            
        },

        "trainzMesh": {
            "title": "Trainz Mesh Composition Wrapper",
            "description": "Container for the specification of a Trainz mesh describing what FBX file and Trainz asset to create from which of your Blender objects.",
            
            "type": "object",
            "required": ["trainzMeshComposition"],
            "additionalProperties": false,

            "properties": {
                "trainzMeshComposition": {
                    "title": "Trainz Mesh Composition Settings",
                    "description": "Specification of a Trainz mesh describing what FBX file and Trainz asset to create from which of your Blender objects. You may include comments as well.",
                    
                    "type": "object",
                    "required": ["name", "blenderExportSpecifications"],
                    "additionalProperties": false,
                    
                    "properties": {

                        "comment": {
                            "$ref": "#/$defs/comment"
                        },

                        "name": {
                            "title": "Trainz Mesh File Name (Pattern)",
                            "description": "Name (pattern) for the FBX files to create.",
                            "type": "string"
                        },

                        "blenderExportSpecifications": {

                            "title": "Blender Export Specifications",
                            "description": "Specifies one or more Blender objects, with optionally assigning a material and UV map to each, to include them in the Trainz mesh FBX file.",
                            
                            "type": "array",
                            "minItems": 1,
                            "additionalItems": false,
                            "items": {
                                "anyOf": [
                                    {   "$ref": "#/$defs/blenderExportSpecification",
                                        "minContains": 1
                                    },
                                    {
                                        "comment": {
                                            "$ref": "#/$defs/comment"
                                        },
                                        "minContains": 0
                                    }
                                ]                                
                            }
                        }            
                    }
                }
            }
        },            


        "blenderExportSpecification": {
            "title": "Blender Export Specification",
            "description": "Specifies one or more Blender objects, with optionally assigning a material and UV map to each, to include them in the Trainz mesh FBX file.",
            
            "type": "object",              
            "anyOf": [
                { "required": ["scopeToCollections"] },
                { "required": ["includeObjectsByName"] },
                { "required": ["includeObjectsByPattern"] },
                { "required": ["includeObjectsByConditions"] },
                { "required": ["excludeObjectsByName"] },
                { "required": ["excludeObjectsByPattern"] },
                { "required": ["excludeObjectsByConditions"] },
                { "required": ["dropModifiers"] },
                { "required": ["material"] },
                { "required": ["uvMap"] }
            ],
            "additionalProperties": false,

            "properties": {

                "scopeToCollections":  {
                    "title": "Scope to Collections",
                    "description": "A regular expression specifying collection name patterns or a list specifying collection names. Only Blender objects contained under referenced collections in your Blender file will be processed for inclusion for exporting.",
                    
                    "oneOf": [
                        {
                            "type": "string"
                        },
                        {
                            "type": "array",
                            "items": {
                                "type": "string"
                            },
                            "minItems": 1
                        }
                    ]
                },

                "includeObjectsByName": {
                    "title": "Include Object(s) By Name",
                    "description": "The name or (in the form of a string array) names of the Blender object(s) to export and to which parameters in this export specification apply.",

                    "oneOf": [
                        {
                            "type": "string"
                        },
                        {
                            "type": "array",
                            "items": {
                                "type": "string"
                            },
                            "minItems": 1
                        }
                    ]
                },

                "includeObjectsByPattern":  {         
                    "title": "Include Objects By Name Pattern",
                    "description": "A regular expression specifying an object name pattern. Only objects within the scope selected by scopeToCollections and matching this pattern will be exported.",
                    "type": "string",
                    "format": "regex"
                },

                "includeObjectsByConditions":  {         
                    "title": "Include Objects By Condition",
                    "description": "A pair of a property name and a value pattern. Only Blender objects having this property with a value matching the specified value pattern will be included in the export.",
                    
                    "type": "array",
                    "minItems": 1,

                    "items": {
                        "$ref": "#/$defs/objectPropertyCondition"
                    }                    
                },

                "excludeObjectsByName": {
                    "title": "Exclude Object(s) By Name",
                    "description": "Name(s) of object(s) to omit when exporting within this export specification.",

                    "oneOf": [
                        {
                            "type": "string" 
                        },
                        {
                            "type": "array",
                            "items": {
                                "type": "string"
                            },
                            "minItems": 1
                        }
                    ]
                },

                "excludeObjectsByPattern":  {         
                    "title": "Exclude Objects By Name Pattern",
                    "description": "A regular expression specifying an object name pattern. Matching objects will be omitted when exporting within this export specification.",
                    "type": "string",
                    "format": "regex"
                },

                "excludeObjectsByConditions":  {         
                    "title": "Exclude Objects By Condition",
                    "description": "A pair of a property name and a value pattern. Blender objects having this property with a value matching the specified value pattern will not be included in the export.",
                    
                    "type": "array",
                    "minItems": 1,

                    "items": {
                        "$ref": "#/$defs/objectPropertyCondition"
                    }                    
                },

                "dropModifiers": {
                    "title": "Modifiers to Drop",
                    "description": "Name(s) of modifiers to hide before exporting the objects.",

                    "oneOf": [
                        {
                            "title": "Name Pattern for Modifiers to Drop",
                            "description": "A regex. Modifiers with a name matching this regex will not be applied.",
                            "type": "string",
                            "format": "regex"                           
                        },
                        {
                            "title": "Names of Modifiers to Drop",
                            "description": "A list of names of modifiers to hide before exporting.",
                            "type": "array",
                            "items": {
                                "type": "string"
                            },
                            "minItems": 1
                        }
                    ]
                },
                
                "material": {
                    "title": "Material to Apply",
                    "description": "Name of the material in your Blender file to apply to the object before exporting. The name shall conform to Trainz specifications.",
                    "type": "string"
                },

                "uvMap": {
                    "title": "UV Map to Apply",
                    "description": "Name of the UV map created for the Blender object to switch to before exporting.",
                    "type": "string"
                }
            }          
        },

        "objectPropertyCondition": {

            "title": "Object Property Condition",
            "description": "Description of a condition composed of a custom property defined for Blender objects and a pattern for matching values",
            
            "required": ["name", "valuePattern"],
            "additionalProperties": false,
            
            "properties": {

                "name": {
                    "title": "Condition Property Name",
                    "description": "Name of the property containing the condition.",
                    "type": "string"
                },

                "valuePattern": {   
                    "title": "Condition Value Pattern",
                    "description": "A pattern which the value of the property shall match to meet the condition.",
                    "type": "string",
                    "format": "regex"
                }
            }            
                 
        }
    }
}

And I have this JSON based on that scheme:

{   
    "trainzMeshes": [
        { "comment": "*****************************************************************" },
        { "comment": "  STATION NAME DISPLAY - SIZE S                                  " },
        { "comment": "*****************************************************************" },
        { "comment": "---------------------------- Lod 0 ------------------------------" },
        {
            "trainzMeshComposition": {
                "name": "name-display-S-daytime-lod0n",
                "blenderExportSpecifications": [
                    {
                        "includeObject": [
                            "Name Display Console - S [lod0]",
                            "Name Display - S - Default [lod0]"
                        ]
                    }
                ]
            }
        }
    ]
}

The validator I have for VS Code, as well as Newtonsoft's validator at https://www.jsonschemavalidator.net/ finds that the JSON conforms to the schema, whereas I would expect "includeObject" to violate it for two reasons.

  1. If the "includeObject" key is considered a part of the subschema "blenderExportSpecification", which has a couple of properties, but none of them is "includeObject", moreover additionalProperties is set to false. So if it's considered as part of "blenderExportSpecification", violation shall occur.
  2. In case the validator finds the object with "includeObject" is not an object of the "blenderExportSpecification" type, I would expect the failure at the array level, as it's not one of the two allowed types.

I tried playing with a lot of things, such as changing "oneOf" to "anyOf", to change the oneOf > items structure to items > oneOf, but no way could I make it fail.


Solution

  • Your anyOf schemas are unconstrained because you defined comment as a property, but you didn't use the properties keyword, which JSON Schema interprets as the property comment is unconstrained. Hopefully that makes sense.

    You also have some invalid usage of minItems, and minContains which goes unused because you are not using contains keyword. Also, the location of these keywords used would be incorrect as a sibling to the $ref.

    try this...

    {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "additionalProperties": false,
        "properties": {
            "trainzMeshes": {
                "title": "Trainz Meshes",
                "description": "Specifications of Trainz meshes to create as FBX files and comments specified as an array.",
                "type": "array",
                "additionalItems": false,
                "items": {
                    "anyOf": [
                        {
                            "type": "object",
                            "additionalProperties": false,
                            "properties": {
                                "comment": {
                                    "$ref": "#/$defs/comment"
                                }
                            }
                        },
                        {
                            "$ref": "#/$defs/trainzMesh"
                        }
                    ]
                }
            }
        },
        "$defs": {
            "comment": {
                "title": "Comment",
                "description": "A comment to help you remember what comes next. You may include as many comments as you wish.",
                "oneOf": [
                    {
                        "type": "string"
                    },
                    {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "minItems": 1
                    }
                ]
            },
            "trainzMesh": {
                "title": "Trainz Mesh Composition Wrapper",
                "description": "Container for the specification of a Trainz mesh describing what FBX file and Trainz asset to create from which of your Blender objects.",
                "type": "object",
                "required": [
                    "trainzMeshComposition"
                ],
                "additionalProperties": false,
                "properties": {
                    "trainzMeshComposition": {
                        "title": "Trainz Mesh Composition Settings",
                        "description": "Specification of a Trainz mesh describing what FBX file and Trainz asset to create from which of your Blender objects. You may include comments as well.",
                        "type": "object",
                        "required": [
                            "name",
                            "blenderExportSpecifications"
                        ],
                        "additionalProperties": false,
                        "properties": {
                            "comment": {
                                "$ref": "#/$defs/comment"
                            },
                            "name": {
                                "title": "Trainz Mesh File Name (Pattern)",
                                "description": "Name (pattern) for the FBX files to create.",
                                "type": "string"
                            },
                            "blenderExportSpecifications": {
                                "title": "Blender Export Specifications",
                                "description": "Specifies one or more Blender objects, with optionally assigning a material and UV map to each, to include them in the Trainz mesh FBX file.",
                                "type": "array",
                                "minItems": 1,
                                "additionalItems": false,
                                "items": {
                                    "anyOf": [
                                        {
                                            "$ref": "#/$defs/blenderExportSpecification"
                                        },
                                        {
                                            "type": "object",
                                            "additionalProperties": false,
                                            "properties": {
                                                "comment": {
                                                    "$ref": "#/$defs/comment"
                                                }
                                            }
                                        }
                                    ]
                                }
                            }
                        }
                    }
                }
            },
            "blenderExportSpecification": {
                "title": "Blender Export Specification",
                "description": "Specifies one or more Blender objects, with optionally assigning a material and UV map to each, to include them in the Trainz mesh FBX file.",
                "type": "object",
                "anyOf": [
                    {
                        "required": [
                            "scopeToCollections"
                        ]
                    },
                    {
                        "required": [
                            "includeObjectsByName"
                        ]
                    },
                    {
                        "required": [
                            "includeObjectsByPattern"
                        ]
                    },
                    {
                        "required": [
                            "includeObjectsByConditions"
                        ]
                    },
                    {
                        "required": [
                            "excludeObjectsByName"
                        ]
                    },
                    {
                        "required": [
                            "excludeObjectsByPattern"
                        ]
                    },
                    {
                        "required": [
                            "excludeObjectsByConditions"
                        ]
                    },
                    {
                        "required": [
                            "dropModifiers"
                        ]
                    },
                    {
                        "required": [
                            "material"
                        ]
                    },
                    {
                        "required": [
                            "uvMap"
                        ]
                    }
                ],
                "additionalProperties": false,
                "properties": {
                    "scopeToCollections": {
                        "title": "Scope to Collections",
                        "description": "A regular expression specifying collection name patterns or a list specifying collection names. Only Blender objects contained under referenced collections in your Blender file will be processed for inclusion for exporting.",
                        "oneOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 1
                            }
                        ]
                    },
                    "includeObjectsByName": {
                        "title": "Include Object(s) By Name",
                        "description": "The name or (in the form of a string array) names of the Blender object(s) to export and to which parameters in this export specification apply.",
                        "oneOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 1
                            }
                        ]
                    },
                    "includeObjectsByPattern": {
                        "title": "Include Objects By Name Pattern",
                        "description": "A regular expression specifying an object name pattern. Only objects within the scope selected by scopeToCollections and matching this pattern will be exported.",
                        "type": "string",
                        "format": "regex"
                    },
                    "includeObjectsByConditions": {
                        "title": "Include Objects By Condition",
                        "description": "A pair of a property name and a value pattern. Only Blender objects having this property with a value matching the specified value pattern will be included in the export.",
                        "type": "array",
                        "minItems": 1,
                        "items": {
                            "$ref": "#/$defs/objectPropertyCondition"
                        }
                    },
                    "excludeObjectsByName": {
                        "title": "Exclude Object(s) By Name",
                        "description": "Name(s) of object(s) to omit when exporting within this export specification.",
                        "oneOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 1
                            }
                        ]
                    },
                    "excludeObjectsByPattern": {
                        "title": "Exclude Objects By Name Pattern",
                        "description": "A regular expression specifying an object name pattern. Matching objects will be omitted when exporting within this export specification.",
                        "type": "string",
                        "format": "regex"
                    },
                    "excludeObjectsByConditions": {
                        "title": "Exclude Objects By Condition",
                        "description": "A pair of a property name and a value pattern. Blender objects having this property with a value matching the specified value pattern will not be included in the export.",
                        "type": "array",
                        "minItems": 1,
                        "items": {
                            "$ref": "#/$defs/objectPropertyCondition"
                        }
                    },
                    "dropModifiers": {
                        "title": "Modifiers to Drop",
                        "description": "Name(s) of modifiers to hide before exporting the objects.",
                        "oneOf": [
                            {
                                "title": "Name Pattern for Modifiers to Drop",
                                "description": "A regex. Modifiers with a name matching this regex will not be applied.",
                                "type": "string",
                                "format": "regex"
                            },
                            {
                                "title": "Names of Modifiers to Drop",
                                "description": "A list of names of modifiers to hide before exporting.",
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 1
                            }
                        ]
                    },
                    "material": {
                        "title": "Material to Apply",
                        "description": "Name of the material in your Blender file to apply to the object before exporting. The name shall conform to Trainz specifications.",
                        "type": "string"
                    },
                    "uvMap": {
                        "title": "UV Map to Apply",
                        "description": "Name of the UV map created for the Blender object to switch to before exporting.",
                        "type": "string"
                    }
                }
            },
            "objectPropertyCondition": {
                "title": "Object Property Condition",
                "description": "Description of a condition composed of a custom property defined for Blender objects and a pattern for matching values",
                "required": [
                    "name",
                    "valuePattern"
                ],
                "additionalProperties": false,
                "properties": {
                    "name": {
                        "title": "Condition Property Name",
                        "description": "Name of the property containing the condition.",
                        "type": "string"
                    },
                    "valuePattern": {
                        "title": "Condition Value Pattern",
                        "description": "A pattern which the value of the property shall match to meet the condition.",
                        "type": "string",
                        "format": "regex"
                    }
                }
            }
        }
    }
    

    The diff

    first anyOf

    second anyOf


    EDIT, based on your comment for additional requirements:

    This is one way to do it.

    "$schema": "http://json-schema.org/draft-07/schema#",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "trainzMeshes": {
          "title": "Trainz Meshes",
          "description": "Specifications of Trainz meshes to create as FBX files and comments specified as an array.",
          "type": "array",
          "minItems": 1,
          "items": [
            {
              "$ref": "#/$defs/trainzMesh"
            }
          ],
          "additionalItems": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "comment": {
                "$ref": "#/$defs/comment"
              }
            }
          }
        }
      }