This section describes when a JSON document is a correct JTD schema. Because Concise Data Definition Language (CDDL) is well suited to the task of defining complex JSON formats, such as JTD schemas, this section uses CDDL to describe the format of JTD schemas.
JTD schemas may recursively contain other schemas. In this document, a "root schema" is one that is not contained within another schema, i.e., it is "top level".
A JTD schema is a JSON object taking on an appropriate form. JTD schemas may contain "additional data", discussed in
Section 2.3. Root JTD schemas may optionally contain definitions (a mapping from names to schemas).
A correct root JTD schema
MUST match the "root-schema" CDDL rule described in this section. A correct non-root JTD schema
MUST match the "schema" CDDL rule described in this section.
; root-schema is identical to schema, but additionally allows for
; definitions.
;
; definitions are prohibited from appearing on non-root schemas.
root-schema = {
? definitions: { * tstr => { schema}},
schema,
}
; schema is the main CDDL rule defining a JTD schema.
;
; All JTD schemas are JSON objects taking on one of eight forms
; listed here.
schema = (
ref //
type //
enum //
elements //
properties //
values //
discriminator //
empty //
)
; shared is a CDDL rule containing properties that all eight schema
; forms share.
shared = (
? metadata: { * tstr => any },
? nullable: bool,
)
; empty describes the "empty" schema form.
empty = shared
; ref describes the "ref" schema form.
;
; There are additional constraints on this form that cannot be
; expressed in CDDL. Section 2.2.2 describes these additional
; constraints in detail.
ref = ( ref: tstr, shared )
; type describes the "type" schema form.
type = (
type: "boolean"
/ "float32"
/ "float64"
/ "int8"
/ "uint8"
/ "int16"
/ "uint16"
/ "int32"
/ "uint32"
/ "string"
/ "timestamp",
shared,
)
; enum describes the "enum" schema form.
;
; There are additional constraints on this form that cannot be
; expressed in CDDL. Section 2.2.4 describes these additional
; constraints in detail.
enum = ( enum: [+ tstr], shared )
; elements describes the "elements" schema form.
elements = ( elements: { schema }, shared )
; properties describes the "properties" schema form.
;
; This CDDL rule is defined so that a schema of the "properties" form
; may omit a member named "properties" or a member named
; "optionalProperties", but not both.
;
; There are additional constraints on this form that cannot be
; expressed in CDDL. Section 2.2.6 describes these additional
; constraints in detail.
properties = (with-properties // with-optional-properties)
with-properties = (
properties: { * tstr => { schema }},
? optionalProperties: { * tstr => { schema }},
? additionalProperties: bool,
shared,
)
with-optional-properties = (
? properties: { * tstr => { schema }},
optionalProperties: { * tstr => { schema }},
? additionalProperties: bool,
shared,
)
; values describes the "values" schema form.
values = ( values: { schema }, shared )
; discriminator describes the "discriminator" schema form.
;
; There are additional constraints on this form that cannot be
; expressed in CDDL. Section 2.2.8 describes these additional
; constraints in detail.
discriminator = (
discriminator: tstr,
; Note well: this rule is defined in terms of the "properties"
; CDDL rule, not the "schema" CDDL rule.
mapping: { * tstr => { properties } }
shared,
)
The remainder of this section will describe constraints on JTD schemas that cannot be expressed in CDDL. It will also provide examples of valid and invalid JTD schemas.
The "root-schema" rule in
Figure 1 permits a member named "definitions", but the "schema" rule does not permit for such a member. This means that only root (i.e., "top-level") JTD schemas can have a "definitions" object, and subschemas may not.
Thus,
is a correct JTD schema, but
{
"definitions": {
"foo": {
"definitions": {}
}
}
}
is not, because subschemas (such as the object at "/definitions/foo") must not have a member named "definitions".
JTD schemas (i.e., JSON objects satisfying the "schema" CDDL rule in
Figure 1) must take on one of eight forms. These forms are defined so as to be mutually exclusive; a schema cannot satisfy multiple forms at once.
The "empty" form is defined by the "empty" CDDL rule in
Figure 1. The semantics of the "empty" form are described in
Section 3.3.1.
Despite the name "empty", schemas of the "empty" form are not necessarily empty JSON objects. Like schemas of any of the eight forms, schemas of the "empty" form may contain members named "nullable" (whose value must be "true" or "false") or "metadata" (whose value must be an object) or both.
Thus,
and
and
{ "nullable": true, "metadata": { "foo": "bar" }}
are correct JTD schemas of the "empty" form, but
is not, because the value of the member named "nullable" must be "true" or "false".
The "ref" form is defined by the "ref" CDDL rule in
Figure 1. The semantics of the "ref" form are described in
Section 3.3.2.
For a schema of the "ref" form to be correct, the value of the member named "ref" must refer to one of the definitions found at the root level of the schema it appears in. More formally, for a schema
S of the "ref" form:
-
Let B be the root schema containing the schema or the schema itself if it is a root schema.
-
Let R be the value of the member of S with the name "ref".
If the schema is correct, then
B MUST have a member
D with the name "definitions", and
D MUST contain a member whose name equals
R.
Thus,
{
"definitions": {
"coordinates": {
"properties": {
"lat": { "type": "float32" },
"lng": { "type": "float32" }
}
}
},
"properties": {
"user_location": { "ref": "coordinates" },
"server_location": { "ref": "coordinates" }
}
}
is a correct JTD schema and demonstrates the point of the "ref" form: to avoid redefining the same thing twice. However,
is not a correct JTD schema, as there are no top-level "definitions", and so the "ref" form cannot be correct. Similarly,
{ "definitions": { "foo": {}}, "ref": "bar" }
is not a correct JTD schema, as there is no member named "bar" in the top-level "definitions".
The "type" form is defined by the "type" CDDL rule in
Figure 1. The semantics of the "type" form are described in
Section 3.3.3.
As an example of a correct JTD schema of the "type" form,
is a correct JTD schema, whereas
and
are not correct schemas, as neither "true" nor the JSON string "foo" are in the list of permitted values of the "type" member described in the "type" CDDL rule in
Figure 1.
The "enum" form is defined by the "enum" CDDL rule in
Figure 1. The semantics of the "enum" form are described in
Section 3.3.4.
For a schema of the "enum" form to be correct, the value of the member named "enum" must be a nonempty array of strings, and that array must not contain duplicate values. More formally, for a schema
S of the "enum" form:
-
Let E be the value of the member of S with name "enum".
If the schema is correct, then there
MUST NOT exist any pair of elements of
E that encode equal string values, where string equality is defined as in
Section 8.3 of
RFC 8259.
Thus,
is not a correct JTD schema, as the value of the member named "enum" must be nonempty, and
{ "enum": ["a\\b", "a\u005Cb"] }
is not a correct JTD schema, as
and
encode strings that are equal by the definition of string equality given in
Section 8.3 of
RFC 8259. By contrast,
{ "enum": ["PENDING", "IN_PROGRESS", "DONE" ]}
is an example of a correct JTD schema of the "enum" form.
The "elements" form is defined by the "elements" CDDL rule in
Figure 1. The semantics of the "elements" form are described in
Section 3.3.5.
As an example of a correct JTD schema of the "elements" form,
{ "elements": { "type": "uint8" }}
is a correct JTD schema, whereas
and
{ "elements": { "type": "foo" } }
are not correct schemas, as neither
nor
are correct JTD schemas, and the value of the member named "elements" must be a correct JTD schema.
The "properties" form is defined by the "properties" CDDL rule in
Figure 1. The semantics of the "properties" form are described in
Section 3.3.6.
For a schema of the "properties" form to be correct, properties must either be required (i.e., in "properties") or optional (i.e., in "optionalProperties"), but not both.
More formally, if a schema has both a member named "properties" (with value
P) and another member named "optionalProperties" (with value
O), then
O and
P MUST NOT have any member names in common; that is, no member of
P may have a name equal to the name of any member of
O, under the definition of string equality given in
Section 8.3 of
RFC 8259.
Thus,
{
"properties": { "confusing": {} },
"optionalProperties": { "confusing": {} }
}
is not a correct JTD schema, as "confusing" appears in both "properties" and "optionalProperties". By contrast,
{
"properties": {
"users": {
"elements": {
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"create_time": { "type": "timestamp" }
},
"optionalProperties": {
"delete_time": { "type": "timestamp" }
}
}
},
"next_page_token": { "type": "string" }
}
}
is a correct JTD schema of the "properties" form, describing a paginated list of users and demonstrating the recursive nature of the syntax of JTD schemas.
The "values" form is defined by the "values" CDDL rule in
Figure 1. The semantics of the "values" form are described in
Section 3.3.7.
As an example of a correct JTD schema of the "values" form,
{ "values": { "type": "uint8" }}
is a correct JTD schema, whereas
and
{ "values": { "type": "foo" } }
are not correct schemas, as neither
nor
are correct JTD schemas, and the value of the member named "values" must be a correct JTD schema.
The "discriminator" form is defined by the "discriminator" CDDL rule in
Figure 1. The semantics of the "discriminator" form are described in
Section 3.3.8. Understanding the semantics of the "discriminator" form will likely aid the reader in understanding why this section provides constraints on the "discriminator" form beyond those in
Figure 1.
To prevent ambiguous or unsatisfiable constraints on the "discriminator" property of a tagged union, an additional constraint on schemas of the "discriminator" form exists. For schemas of the "discriminator" form:
-
Let D be the member of the schema with the name "discriminator".
-
Let M be the member of the schema with the name "mapping".
If the schema is correct, then all member values
S of
M will be schemas of the "properties" form. For each
S:
-
If S has a member N whose name equals "nullable", N's value MUST NOT be the JSON primitive value "true".
-
For each member P of S whose name equals "properties" or "optionalProperties", P's value, which must be an object, MUST NOT contain any members whose name equals D's value.
Thus,
{
"discriminator": "event_type",
"mapping": {
"can_the_object_be_null_or_not?": {
"nullable": true,
"properties": { "foo": { "type": "string" } }}
}
}
}
is an incorrect schema, as a member of "mapping" has a member named "nullable" whose value is "true". This would suggest that the instance may be null. Yet, the top-level schema lacks such a "nullable" set to "true", which would suggest that the instance in fact cannot be null. If this were a correct JTD schema, it would be unclear which piece of information takes precedence.
JTD handles such possible ambiguity by disallowing, at the syntactic level, the possibility of contradictory specifications of whether an instance described by a schema of the "discriminator" form may be null. The schemas in a discriminator "mapping" cannot have "nullable" set to "true"; only the discriminator itself can use "nullable" in this way.
It also follows that
{
"discriminator": "event_type",
"mapping": {
"is_event_type_a_string_or_a_float32?": {
"properties": { "event_type": { "type": "float32" }}
}
}
}
and
{
"discriminator": "event_type",
"mapping": {
"is_event_type_a_string_or_an_optional_float32?": {
"optionalProperties": { "event_type": { "type": "float32" }}
}
}
}
are incorrect schemas, as "event_type" is both the value of "discriminator" and a member name in one of the "mapping" member "properties" or "optionalProperties". This is ambiguous, because ordinarily the "discriminator" keyword would indicate that "event_type" is expected to be a string, but another part of the schema specifies that "event_type" is expected to be a number.
JTD handles such possible ambiguity by disallowing, at the syntactic level, the possibility of contradictory specifications of discriminator "tags". Discriminator "tags" cannot be redefined in other parts of the schema.
By contrast,
{
"discriminator": "event_type",
"mapping": {
"account_deleted": {
"properties": {
"account_id": { "type": "string" }
}
},
"account_payment_plan_changed": {
"properties": {
"account_id": { "type": "string" },
"payment_plan": { "enum": ["FREE", "PAID"] }
},
"optionalProperties": {
"upgraded_by": { "type": "string" }
}
}
}
}
is a correct schema, describing a pattern of data common in JSON-based messaging systems.
Section 3.3.8 provides examples of what this schema accepts and rejects.
This document does not describe any extension mechanisms for JTD schema validation, which is described in
Section 3. However, schemas are defined to optionally contain a "metadata" keyword, whose value is an arbitrary JSON object. Call the members of this object "metadata members".
Users
MAY add metadata members to JTD schemas to convey information that is not pertinent to validation. For example, such metadata members could provide hints to code generators or trigger some special behavior for a library that generates user interfaces from schemas.
Users
SHOULD NOT expect metadata members to be understood by other parties. As a result, if consistent validation with other parties is a requirement, users
MUST NOT use metadata members to affect how schema validation, as described in
Section 3, works.
Users
MAY expect metadata members to be understood by other parties and
MAY use metadata members to affect how schema validation works, if these other parties are somehow known to support these metadata members. For example, two parties may agree, out of band, that they will support an extended JTD with a custom metadata member that affects validation.