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.