Syntax Reference JSON Schema

Page Summary

On this page we discuss JSON Schema and how it’s used in JADE Editor instances.

JSON Schema Intro

JSON Schema is, first and foremost, just JSON. However, it is JSON which describes the structure and type which must be matched by other JSON objects. This is useful for validating that a given JSON object matches any desired structural and typing requirements, such as plugin configurations. The JADE Editor uses JSON Schema to validate that plugin configurations match the required format and type; it’s what powers many of the error messages and even the hover help in editor instances. When you create your own plugins, you will create a JSON Schema file which describes the valid structure and type of the configuration options you wish to allow for your plugin.

Basic JSON Schema Syntax

Let’s start with a simple example of JSON Schema which describes a JSON object with a number named “num” and a boolean named “bool”:

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool"],
    "properties": {
        "num": {"type": "number", "description": "A number."},
        "bool": {"type": "boolean", "description": "A boolean."}
    }
}

The top level keys type, description, required, and properties are keywords in JSON Schema. Under properties notice we have the elements num and bool, each of which has it’s own little “sub-schema” to define its type and description. So the schema above describes a JSON object which is required to have keys num and bool.

The schema above would validate the following JSON object without errors:

{
    "num": 10.5,
    "bool": true
}

However, it would NOT validate the following JSON object because the required key bool is missing:

{
    "num": 10.5
}

The following JSON object would also NOT validate, in this case because bool is defined as a string instead of a boolean type:

{
    "num": 10.5,
    "bool": "true"
}

Notably, the required keyword is optional and used for elements of type object to specify which keys of the object are required (i.e. displays errors for any required keys which are missing in any JSON we’re validating against our schema).

How JSON Schema Is Used

JADE Editor instances use JSON Schema to provide plugin configuration help. In particular, the editor will display errors for any parts of configuration which don’t match the JSON Schema defined for the plugin. Furthermore, editors will also display the description text when hovering over json elements in the editor. In fact, we even generate the big filterable list of configuration options in the JADE docs for each plugin type from the underlying schemas. As we shall see, JSON Schema provides many more “constraints” on what we want to allow and disallow in our JSON plugin configurations.

More JSON Schema Syntax

The JSON Schema standard is now several generations into its lifetime and supports many useful features for validating JSON objects. Let’s look at some of those features one by one, with the idea that they can all be combined as needed in larger schemas.

Numeric Ranges

We can constrain the range of numeric values by adding minimum and maximum keys to numeric sub-schemas. Here’s how our original schema above would look if we wanted to constrain num to be in the range 0-100, where we have also updated the description:

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool"],
    "properties": {
        "num": {"type": "number", "description": "A number between 0 and 100.", "minimum": 0, "maximum": 100},
        "bool": {"type": "boolean", "description": "A boolean."}
    }
}

Default Values

We can specify default values for any elements defined in JSON Schema, at any level. These default values are used to generate the initial plugin configuration when new plugins are added to an application in JADE. Here’s how we could specify that num should have a defalut value of 0 and bool should have a default value of false in our original schema above:

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool"],
    "properties": {
        "num": {"type": "number", "description": "A number.", "default": 0},
        "bool": {"type": "boolean", "description": "A boolean.", "default": false}
    }
}

String Elements

So far we’ve seen numeric and boolean element types. We can of course specify elements of type string as shown with the addition of the string element named str below:

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool", "str"],
    "properties": {
        "num": {"type": "number", "description": "A number.", "default": 0},
        "bool": {"type": "boolean", "description": "A boolean.", "default": false},
        "str": {"type": "string", "description": "A string.", "default": "Hello Kitty!"},
    }
}

Array Elements

We can also specify elements of type array as shown with the addition of the array element named arr below. More specifically, we constrain the items of the array to be of type string:

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool", "arr"],
    "properties": {
        "num": {"type": "number", "description": "A number.", "default": 0},
        "bool": {"type": "boolean", "description": "A boolean.", "default": false},
        "arr": {
            "type": "array", 
            "description": "An array.", 
            "default": [],
            "items": { "type": "string", "description": "String array element." }
        }
    }
}

Notice that items is simply a sub-schema, so it can be any type with arbitrary depth. Here’s an example of an object which would validate against the schema above:

{
    "num": 10.5,
    "bool": true,
    "arr": ["Jack", "Jill"]
}

Object Elements

Of course, we can also specify elements of type object as shown with the addition of the object element named obj below.

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool", "obj"],
    "properties": {
        "num": {"type": "number", "description": "A number.", "default": 0},
        "bool": {"type": "boolean", "description": "A boolean.", "default": false},
        "obj": {
            "type": "object", 
            "description": "An object.", 
            "default": {},
            "required": ["animal", "color", "age"],
            "properties": {
                "animal": { "type": "string", "description": "An animal.", "default": "Bear" },
                "color": { "type": "string", "description": "Color of the animal.", "default": "Brown" },
                "age": { "type": "integer": 3, "description": "The animal's age.", "default": 0 }
            }
        }
    }
}

Notice that we take this moment to introduce the integer element type for the age. This inherently constrains the age to be an integer, so that attempting to put a value of, say, 3.5 into configuration cause an error to display in the Editor.

Enumerated Elements

We can also specify elements of type enum, which essentially constrains values to be one of a finite set of values we specify. Below we show an example where the enum named pet has values kitty, puppy, and bunny.

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool", "pet"],
    "properties": {
        "num": {"type": "number", "description": "A number.", "default": 0},
        "bool": {"type": "boolean", "description": "A boolean.", "default": false},
        "pet": { "enum":  ["kitty", "puppy", "bunny"], "description": "An array.", "default": "kitty" }
    }
}

Notice that we don’t need to specify a type key for the pet element here, since the allowed values are explicitly provided in the enum. Also notice that the valid values defined in the enum are captured as a little array. This does not mean that the value of pet can be an array, it just the way JSON Schema allows us to define the valid values for our enum. One nice editor feature of enum elements is that the editor will auto-complete to match the values defined in the enum as you type. Ultimately what matters here is that when editing a plugin configuration, only the values specified in the enum are allowed (in this case, the strings kitty, puppy, and bunny).

OneOf Keyword

The oneOf keyword allows us to specify that an element in our JSON configuration can be one of several types. This is a little like the enum except that instead of having a list of explicit values, we get to define a list of types which are valid. This can be helpful when a configuration option can accept multiple types, such as different kinds of triggering options for a data acquisition task.

We show a simple oneOf example below where we want to allow shape to be either an object defining a triangle or circle:

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool", "shape"],
    "properties": {
        "num": {"type": "number", "description": "A number.", "default": 0},
        "bool": {"type": "boolean", "description": "A boolean.", "default": false},
        "shape": {
            "type": "object", 
            "description": "A shape (either a triangle or circle).",
            "required": ["kind"],
            "oneOf": [
                {
                    "kind": { "enum": ["triangle"], "description": "A triangle.", "default": "triangle" },
                    "point1": { 
                        "type": "object", 
                        "description": "The first triangle point.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point1", "default": 0 },
                            "y": { "type": "number", "description": "y-coordinate of point1", "default": 0 }
                        }
                    },
                    "point2": { 
                        "type": "object", 
                        "description": "The second triangle point.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point2", "default": 1 },
                            "y": { "type": "number", "description": "y-coordinate of point2", "default": 0 }
                        }
                    },
                    "point3": { 
                        "type": "object", 
                        "description": "The third triangle point.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point3", "default": 0 },
                            "y": { "type": "number", "description": "y-coordinate of point3", "default": 1 }
                        }
                    }
                },
                {
                    "kind": { "enum": ["circle"], "description": "A circle.", "default": "circle" },
                    "radius": { "type": "number", "description": "The radius of the circle.", "default": 1 },
                    "center": { 
                        "type": "object", 
                        "description": "The center of the circle.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point1", "default": 0 },
                            "y": { "type": "number", "description": "y-coordinate of point1", "default": 0 }
                        }
                    }
                }
            }
        }
    }
}

There are a few points to discuss here. First, we see that oneOf is an array of schemas. Any JSON object which validates against this schema must have a shape element which matches one of the schemas within our oneOf definition here.

Next, notice that shape is specified to be of type object. This means that all of the elements we put in the oneOf definition must be objects.

Furthermore, notice that we made the kind key be required for all choices in our oneOf.

And finally, notice that we made our kind elements just a single value: triangle in one case and circle in the other (with those single values as the default values as well). This is a trick to get the editor to do the enum auto-complete trick for us and ensure that triangle only goes with the three points and circle only goes with radius and center elements).

The following JSON objects would each validate against our schema:

{
    "num": 10.5,
    "bool": true,
    "shape": {
        "kind": "triangle",
        "point1": {"x": 0, "y": 0},
        "point2": {"x": 1, "y": 2},
        "point3": {"x": 1, "y": 0}
    }
}
{
    "num": 10.5,
    "bool": true,
    "shape": {
        "kind": "circle",
        "radius": 3,
        "center": {"x": 1, "y": 2}
    }
}

However the following JSON objects would NOT validate against our schema:

{
    "num": 10.5,
    "bool": true,
    "shape": {
        "kind": "triangle",
        "radius": 3,
        "center": {"x": 1, "y": 2}
    }
}

The JSON object above does NOT validate because triangle is incompatible with radius and center elements based on the schema.

{
    "num": 10.5,
    "bool": true,
    "shape": {
        "kind": "bigCircle",
        "radius": 100,
        "center": {"x": 1, "y": 2}
    }
}

The JSON object above does NOT validate because bigCircle is not a valid kind.

JADE Extensions to JSON Schema

Earlier, we briefly noted that a plugin’s JSON Schema is not only used to provide feedback / errors in JADE Editor instances, but also to generate the big filterable list of configuration options in plugin docs. We use JSON path notation to “flatten” out all of the possible options with descriptions and valid types. For example, consider this JSON object:

{
    "num": 10.5,
    "bool": true,
    "shape": {
        "kind": "circle",
        "radius": 3,
        "center": {"x": 1, "y": 2}
    }
}

The JSON path for the radius here would be: shape.radius. The . between shape and radius here represents object hierarchy. But for the triangle case, shape.radius would not exist. Similarly, for the circle case, shape.point1.x would not exist. So if we’re trying to list out all the possible configuration element paths, we want a way to clearly see that point1 is part of the triangle case and radius is part of the circle case, etc.

To overcome this nuance, we have extended the JSON Schema by adding a displayKey element for any objects defined in a oneOf section, such as shown in the JSON Schema below. Notice the displayKey element just above the kind elements inside the oneOf:

{
    "type": "object",
    "description": "An object with a number named 'num' and boolean named 'bool'.",
    "required": ["num", "bool", "shape"],
    "properties": {
        "num": {"type": "number", "description": "A number.", "default": 0},
        "bool": {"type": "boolean", "description": "A boolean.", "default": false},
        "shape": {
            "type": "object", 
            "description": "A shape (either a triangle or circle).",
            "required": ["kind"],
            "oneOf": [
                {
                    "displayKey": "Triangle",
                    "kind": { "enum": ["triangle"], "description": "A triangle.", "default": "triangle" },
                    "point1": { 
                        "type": "object", 
                        "description": "The first triangle point.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point1", "default": 0 },
                            "y": { "type": "number", "description": "y-coordinate of point1", "default": 0 }
                        }
                    },
                    "point2": { 
                        "type": "object", 
                        "description": "The second triangle point.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point2", "default": 1 },
                            "y": { "type": "number", "description": "y-coordinate of point2", "default": 0 }
                        }
                    },
                    "point3": { 
                        "type": "object", 
                        "description": "The third triangle point.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point3", "default": 0 },
                            "y": { "type": "number", "description": "y-coordinate of point3", "default": 1 }
                        }
                    }
                },
                {
                    "displayKey": "Circle",
                    "kind": { "enum": ["circle"], "description": "A circle.", "default": "circle" },
                    "radius": { "type": "number", "description": "The radius of the circle.", "default": 1 },
                    "center": { 
                        "type": "object", 
                        "description": "The center of the circle.",
                        "required": ["x", "y"],
                        "properties": {
                            "x": { "type": "number", "description": "x-coordinate of point1", "default": 0 },
                            "y": { "type": "number", "description": "y-coordinate of point1", "default": 0 }
                        }
                    }
                }
            }
        }
    }
}

Then, when we list out the possible configuration JSON paths for a plugin, we can distinguish between the consistent groupings oneOf options by injecting the displayKey into the path with syntax like the following:

shape{Triangle}.point1.x
shape{Triangle}.point1.y
shape{Triangle}.point2.x
shape{Triangle}.point2.y
shape{Triangle}.point3.x
shape{Triangle}.point3.y
shape{Circle}.radius
shape{Circle}.center

That is, we simply inject {Triangle} and {Circle} to make it clear which properties are part of which oneOf option.

Closing Remarks

There’s more to the JSON Schema story in general and several great online resources dive deeper. But this is essentially all you need to create JSON SChemas for your custom plugins. In practice, JSON Schema files can get a little large and difficult for the eyes to quickly process because of all the layering in describing the types and options. But they are easy and systematic to create with a little practice, and the payoff is significant given all of the editor help and documentation integration that then comes for free for the users of your custom plugins.