Conditional schema with polymorphism

We now have a flexible and reasonably efficient way of declaring basic structure but, as we delve into more complex schemas, we need to think about how to introduce conditional schema definitions. In XML Schema, tags have content models including <sequence> and <choice>, and this may seem to be an attractive capability. At the same time, XML differs fundamentally from JSON in that a named property of an object can only occur once, whereas an XML tag may contain any number of instances of the same tag. The concept of a “choice” of sub-schemas remains relevant, but it is perhaps better to see this in the context of OO. We can allow a sub-schema to vary by using the OO concept of polymorphism, whereby an instance of a subclass is also an instance of its superclass.

JSD is fundamentally Object-Oriented (OO) by design, and so JSD is itself defined using OO concepts. At the same time, Javascript is a (very) weakly typed OO language and neither requires nor supports the full richness of the UML interpretation of OO. As such, JSD only uses the concept of “classes” to define structure and (noting that OO encapsulates both state and function, whereas JSON is purely a representation of data) does not need more complex concepts such as interfaces. Interfaces, however, represent a vital component of how real-world data actually works, as it separates “strong inheritance” from “weak inheritance”.  A subclass that inherits from a superclass shares the identity of the superclass, as embodied by the concept “X is a Y”. A class that realises (implements) an interface has the characteristics of the interface, but its core identity is still that of its superclass (or simply itself).

To illustrate this with a simple example, we will add a property of our Person that defines their owned vehicle that can be either a car or a bycicle. We will also make the document an array of Persons such that we can illustrate conditionality in a single document, thus:

[
    {
        "first-name": "John",
        "last-name": "Doe",
        "age": 58,
        "vehicle": {
            "bicycle": {
                "make": "Marin",
                "gears": 12
            }
        }
    },
    {
        "first-name": "Jane",
        "last-name": "Doe",
        "age": 51,
        "vehicle": {
            "car": {
                "make": "Peugeot",
                "fuel": "Diesel"
            }
        }
    }
]

This is obviously a contrived example, and there are different OO mechanisms that can be used here to achieve slightly different styles of outcome, but a JSD schema to represent this could be:

{
    "$schema": "https://whitecottage.org.uk/JSD/2025-06/",
    "target-schema": "https://whitecottage.org.uk/person",
    "schema-location": "https://whitecottage.org.uk/person/person.jsd",
    "classes": [
        {
            "name": "vehicle"
        },
        {
            "name": "bicycle",
            "extends": "vehicle",
            "properties": [
                {
                    "name": "bicycle",
                    "object": {
                        "properties": [
                            {
                                "name": "make",
                                "string": {}
                            },
                            {
                                "name": "gears",
                                "number": {}
                            }
                        ]
                    }
                }
            ]
        },
        {
            "name": "car",
            "extends": "vehicle",
            "properties": [
                {
                    "name": "car",
                    "object": {
                        "properties": [
                            {
                                "name": "make",
                                "string": {}
                            },
                            {
                                "name": "fuel",
                                "string": {}
                            }
                        ]
                    }
                {
            ]
        }
    ]
    "object": {
        "properties": [
            {
                "name": "first-name",
                "string": {}
            },
            {
                "name": "last-name",
                "string": {}
            },
            {
                "name": "age",
                "number": {}
            },
            {
                "name": "vehicle",
                "object": {
                    "class": "vehicle"
                }
            }
        ]
    }
}

A minor issue with this contrived schema is that we had to create a “vehicle” object to contain a “bicycle” property or a “car” property. Furthermore, we can’t handle inherited properties (the “make” property is common to both bicycles and cars) cleanly. A better solution would be to use a “type” property to identify the type of the vehicle, but this would require us to be able to specify a literal string value. Fortunately, the “string” type qualifying object makes this simple through the addition of the “value” property. While we’re at it, we will also define a “values” property as an array of valid values to create string enumerations. We also really want to identify “vehicle” as an abstract class, so we will include a boolean “abstract” property in the class definition. Now we can create a simpler and more modular schema like this:

{
    "$schema": "https://whitecottage.org.uk/JSD/2025-06/",
    "target-schema": "https://whitecottage.org.uk/person",
    "schema-location": "https://whitecottage.org.uk/person/person.jsd",
    "classes": [
        {
            "name": "vehicle",
            "abstract": true,
            "properties": [
                {
                    "name": "make",
                    "string": {}
                }
            ]
        },
        {
            "name": "bicycle",
            "extends": "vehicle",
            "properties": [
                {
                    "name": "type",
                    "string": {
                        "value": "bicycle"
                    }
                },
                {
                    "name": "gears",
                    "number": {}
                }
            ]
        },
        {
            "name": "car",
            "extends": "vehicle",
            "properties": [
                {
                    "name": "type",
                    "string": {
                        "value": "car"
                    }
                },
                {
                    "name": "fuel",
                    "string": {}
                }
            ]
        }
    ]
    "object": {
        "properties": [
            {
                "name": "first-name",
                "string": {}
            },
            {
                "name": "last-name",
                "string": {}
            },
            {
                "name": "age",
                "number": {}
            },
            {
                "name": "vehicle",
                "object": {
                    "class": "vehicle"
                }
            }
        ]
    }
}

A neater instance document can now look like this:

[
    {
        "first-name": "John",
        "last-name": "Doe",
        "age": 58,
        "vehicle": {
            "type": "bicycle"
            "make": "Marin",
            "gears": 18
        }
    },
    {
        "first-name": "Jane",
        "last-name": "Doe",
        "age": 51,
        "vehicle": {
            "type": "car"
            "make": "Peugeot",
            "fuel": "Diesel"
        }
    }
]

It might be tempting to encode the type information in the property, rather than being forced to use a “vehicle” property containing a “type” qualifier, but JSON makes this very difficult because it is explicitly not typed. Perhaps more significantly, if the type is only encoded into a property name, then it will not be possible to use such an object in an array context (as only JSON objects have named properties).

For convenience, we can choose to automate the typing of named classes by adding an optional boolean property to the class definition, such as “typed”, such that the “type” property is considered to be automatically generated with the literal value of the class name. For completeness, we could also allow the type property name to be additionally specified. At this point, we will also introduce the concept of default values as a property within each primitive type object (“string”: {}, “number”: {}, “boolean”: {}).

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.