RFC 8927 | JSON Type Definition | November 2020 |
Carion | Experimental | [Page] |
This document proposes a format, called JSON Type Definition (JTD), for describing the shape of JavaScript Object Notation (JSON) messages. Its main goals are to enable code generation from schemas as well as portable validation with standardized error indicators. To this end, JTD is intentionally limited to be no more expressive than the type systems of mainstream programming languages. This intentional limitation, as well as the decision to make JTD schemas be JSON documents, makes tooling atop of JTD easier to build.¶
This document does not have IETF consensus and is presented here to facilitate experimentation with the concept of JTD.¶
This document is not an Internet Standards Track specification; it is published for examination, experimental implementation, and evaluation.¶
This document defines an Experimental Protocol for the Internet community. This is a contribution to the RFC Series, independently of any other RFC stream. The RFC Editor has chosen to publish this document at its discretion and makes no statement about its value for implementation or deployment. Documents approved for publication by the RFC Editor are not candidates for any level of Internet Standard; see Section 2 of RFC 7841.¶
Information about the current status of this document, any errata, and how to provide feedback on it may be obtained at https://www.rfc-editor.org/info/rfc8927.¶
Copyright (c) 2020 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document.¶
This document describes a schema language for JSON [RFC8259] called JSON Type Definition (JTD).¶
There exist many options for describing JSON data. JTD's niche is to focus on enabling code generation from schemas; to this end, JTD's expressiveness is intentionally limited to be no more powerful than what can be expressed in the type systems of mainstream programming languages.¶
The goals of JTD are to:¶
JTD is intentionally designed as a rather minimal schema language. Thus, although JTD can describe some categories of JSON, it is not able to describe its own structure; this document uses Concise Data Definition Language (CDDL) [RFC8610] to describe JTD's syntax. By keeping the expressiveness of the schema language minimal, JTD makes code generation and standardized error indicators easier to implement.¶
Examples in this document use constructs from the C++ programming language. These examples are provided to aid the reader in understanding the principles of JTD but are not limiting in any way.¶
JTD's feature set is designed to represent common patterns in JSON-using applications, while still having a clear correspondence to programming languages in widespread use. Thus, JTD supports:¶
The principle of common patterns in JSON is why JTD does not support 64-bit integers, as these are usually transmitted over JSON in non-interoperable (i.e., ignoring the recommendations in Section 2.2 of [RFC7493]) or mutually inconsistent ways. Appendix A.1 further elaborates on why JTD does not support 64-bit integers.¶
The principle of clear correspondence to common programming languages is why JTD does not support, for example, a data type for integers up to 2**53-1.¶
It is expected that for many use cases, a schema language of JTD's expressiveness is sufficient. Where a more expressive language is required, alternatives exist in CDDL and others.¶
This document does not have IETF consensus and is presented here to facilitate experimentation with the concept of JTD. The purpose of the experiment is to gain experience with JTD and to possibly revise this work accordingly. If JTD is determined to be a valuable and popular approach, it may be taken to the IETF for further discussion and revision.¶
This document has the following structure. Section 2 defines the syntax of JTD. Section 3 describes the semantics of JTD; this includes determining whether some data satisfies a schema and what error indicators should be produced when the data is unsatisfactory. Appendix A discusses why certain features are omitted from JTD. Appendix B presents various JTD schemas and their CDDL equivalents.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
The term "JSON Pointer", when it appears in this document, is to be understood as it is defined in [RFC6901].¶
The terms "object", "member", "array", "number", "name", and "string" in this document are to be interpreted as described in [RFC8259].¶
The term "instance", when it appears in this document, refers to a JSON value being validated against a JTD schema. This value can be an entire JSON document, or it can be a value embedded within a JSON document.¶
JTD is an experiment. Participation in this experiment consists of using JTD to validate or document interchanged JSON messages or building tooling atop of JTD. Feedback on the results of this experiment may be emailed to the author. Participants in this experiment are anticipated to mostly be nodes that provide or consume JSON-based APIs.¶
Nodes know if they are participating in the experiment if they are validating JSON messages against a JTD schema or if they are relying on another node to do so. Nodes are also participating in the experiment if they are running code generated from a JTD schema.¶
The risk of this experiment "escaping" takes the form of a JTD-supporting node expecting another node, which lacks such support, to validate messages against some JTD schema. In such a case, the outcome will likely be that the nodes fail to interchange information correctly.¶
This experiment will be deemed successful when JTD has been implemented by multiple independent parties and these parties successfully use JTD to facilitate information interchange within their internal systems or between systems operated by independent parties.¶
If this experiment is deemed successful, and JTD is determined to be a valuable and popular approach, it may be taken to the IETF for further discussion and revision. One possible outcome of this discussion and revision could be that a working group produces a Standards Track specification of JTD.¶
Some implementations of JTD, as well as code generators and other tooling related to JTD, are available at <https://github.com/jsontypedef>.¶
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.¶
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,¶
{ "definitions": {} }¶
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¶
{ "nullable": true }¶
and¶
{ "nullable": true, "metadata": { "foo": "bar" }}¶
are correct JTD schemas of the "empty" form, but¶
{ "nullable": "foo" }¶
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:¶
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,¶
{ "ref": "foo" }¶
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,¶
{ "type": "uint8" }¶
is a correct JTD schema, whereas¶
{ "type": true }¶
and¶
{ "type": "foo" }¶
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:¶
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 [RFC8259].¶
Thus,¶
{ "enum": [] }¶
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¶
"a\\b"¶
and¶
"a\u005Cb"¶
encode strings that are equal by the definition of string equality given in Section 8.3 of [RFC8259]. 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¶
{ "elements": true }¶
and¶
{ "elements": { "type": "foo" } }¶
are not correct schemas, as neither¶
true¶
nor¶
{ "type": "foo" }¶
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 [RFC8259].¶
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¶
{ "values": true }¶
and¶
{ "values": { "type": "foo" } }¶
are not correct schemas, as neither¶
true¶
nor¶
{ "type": "foo" }¶
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:¶
If the schema is correct, then all member values S of M will be schemas of the "properties" form. For each S:¶
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.¶
This section describes when an instance is valid against a correct JTD schema and the error indicators to produce when an instance is invalid.¶
Users will have different desired behavior with respect to "unspecified" members in an instance. For example, consider the JTD schema in Figure 2:¶
Some users may expect that¶
{"a": "foo", "b": "bar"}¶
satisfies the schema in Figure 2. Others may disagree, as "b" is not one of the properties described in the schema. In this document, allowing such "unspecified" members, like "b" in this example, happens when evaluation is in "allow additional properties" mode.¶
Evaluation of a schema does not allow additional properties by default, but this can be overridden by having the schema include a member named "additionalProperties", where that member has a value of "true".¶
More formally, evaluation of a schema S is in "allow additional properties" mode if there exists a member of S whose name equals "additionalProperties" and whose value is a boolean "true". Otherwise, evaluation of S is not in "allow additional properties" mode.¶
See Section 3.3.6 for how allowing unknown properties affects schema evaluation, but briefly, the schema¶
{ "properties": { "a": { "type": "string" }}}¶
rejects¶
{ "a": "foo", "b": "bar" }¶
However, the schema¶
{ "additionalProperties": true, "properties": { "a": { "type": "string" }} }¶
accepts¶
{ "a": "foo", "b": "bar" }¶
Note that "additionalProperties" does not get "inherited" by subschemas. For example, the JTD schema¶
{ "additionalProperties": true, "properties": { "a": { "properties": { "b": { "type": "string" } } } } }¶
accepts¶
{ "a": { "b": "c" }, "foo": "bar" }¶
but rejects¶
{ "a": { "b": "c", "foo": "bar" }}¶
because the "additionalProperties" at the root level does not affect the behavior of subschemas.¶
Note from Figure 1 that only schemas of the "properties" form may have a member named "additionalProperties".¶
To facilitate consistent validation error handling, this document specifies a standard error indicator format. Implementations SHOULD support producing error indicators in this standard form.¶
The standard error indicator format is a JSON array. The order of the elements of this array is not specified. The elements of this array are JSON objects with:¶
The values for "instancePath" and "schemaPath" depend on the form of the schema and are described in detail in Section 3.3.¶
This section describes, for each of the eight JTD schema forms, the rules dictating whether an instance is accepted, as well as the error indicators to produce when an instance is invalid.¶
The forms a correct schema may take on are formally described in Section 2.¶
The "empty" form is meant to describe instances whose values are unknown, unpredictable, or otherwise unconstrained by the schema. The syntax of the "empty" form is described in Section 2.2.1.¶
If a schema is of the "empty" form, then it accepts all instances. A schema of the "empty" form will never produce any error indicators.¶
The "ref" form is for when a schema is defined in terms of something in the "definitions" of the root schema. The "ref" form enables schemas to be less repetitive and also enables describing recursive structures. The syntax of the "ref" form is described in Section 2.2.2.¶
If a schema is of the "ref" form, then:¶
If the schema has a member named "nullable" whose value is the boolean "true", and the instance is the JSON primitive value "null", then the schema accepts the instance.¶
Otherwise:¶
The schema accepts the instance if and only if S accepts the instance. Otherwise, the error indicators to return in this case are the union of the error indicators from evaluating S against the instance.¶
For example, the schema¶
{ "definitions": { "a": { "type": "float32" }}, "ref": "a" }¶
accepts¶
123¶
but rejects¶
null¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/definitions/a/type" }]¶
The schema¶
{ "definitions": { "a": { "type": "float32" }}, "ref": "a", "nullable": true }¶
accepts¶
null¶
because the schema has a "nullable" member whose value is "true".¶
Note that "nullable" being "false" has no effect in any of the forms described in this document. For example, the schema¶
{ "definitions": { "a": { "nullable": false, "type": "float32" }}, "ref": "a", "nullable": true }¶
accepts¶
null¶
In other words, it is not the case that putting a "false" value for "nullable" will ever override a "nullable" member in schemas of the "ref" form; it is correct, though ineffectual, to have a value of "false" for the "nullable" member in a schema.¶
The "type" form is meant to describe instances whose value is a boolean, number, string, or timestamp [RFC3339]. The syntax of the "type" form is described in Section 2.2.3.¶
If a schema is of the "type" form, then:¶
If the schema has a member named "nullable" whose value is the boolean "true", and the instance is the JSON primitive value "null", then the schema accepts the instance.¶
Otherwise:¶
If "T" equals ... | then the instance is accepted if it is ... |
---|---|
boolean | equal to "true" or "false" |
float32 | a JSON number |
float64 | a JSON number |
int8 | See Table 2 |
uint8 | See Table 2 |
int16 | See Table 2 |
uint16 | See Table 2 |
int32 | See Table 2 |
uint32 | See Table 2 |
string | a JSON string |
timestamp | a JSON string that follows the standard format described in [RFC3339], as refined by Section 3.3 of [RFC4287] |
"float32" and "float64" are distinguished from each other in their intent. "float32" indicates data intended to be processed as an IEEE 754 single-precision float, whereas "float64" indicates data intended to be processed as an IEEE 754 double-precision float. Tools that generate code from JTD schemas will likely produce different code for "float32" than for "float64".¶
If T starts with "int" or "uint", then the instance is accepted if and only if it is a JSON number encoding a value with zero fractional part. Depending on the value of T, this encoded number must additionally fall within a particular range:¶
"T" | Minimum Value (Inclusive) | Maximum Value (Inclusive) |
---|---|---|
int8 | -128 | 127 |
uint8 | 0 | 255 |
int16 | -32,768 | 32,767 |
uint16 | 0 | 65,535 |
int32 | -2,147,483,648 | 2,147,483,647 |
uint32 | 0 | 4,294,967,295 |
Note that¶
10¶
and¶
10.0¶
and¶
1.0e1¶
encode values with zero fractional part, whereas¶
10.5¶
encodes a number with a non-zero fractional part. Thus, the schema¶
{"type": "int8"}¶
accepts¶
10¶
and¶
10.0¶
and¶
1.0e1¶
but rejects¶
10.5¶
as well as¶
false¶
because "false" is not a number at all.¶
If the instance is not accepted, then the error indicator for this case shall have an "instancePath" pointing to the instance and a "schemaPath" pointing to the schema member with the name "type".¶
For example, the schema¶
{"type": "boolean"}¶
accepts¶
false¶
but rejects¶
127¶
The schema¶
{"type": "float32"}¶
accepts¶
10.5¶
and¶
127¶
but rejects¶
false¶
The schema¶
{"type": "string"}¶
accepts¶
"1985-04-12T23:20:50.52Z"¶
and¶
"foo"¶
but rejects¶
false¶
The schema¶
{"type": "timestamp"}¶
accepts¶
"1985-04-12T23:20:50.52Z"¶
but rejects¶
"foo"¶
and¶
false¶
The schema¶
{"type": "boolean", "nullable": true}¶
accepts¶
null¶
and¶
false¶
but rejects¶
127¶
In all of the examples of rejected instances given in this section, the error indicator to produce is:¶
[{ "instancePath": "", "schemaPath": "/type" }]¶
The "enum" form is meant to describe instances whose value must be one of a given set of string values. The syntax of the "enum" form is described in Section 2.2.4.¶
If a schema is of the "enum" form, then:¶
If the schema has a member named "nullable" whose value is the boolean "true", and the instance is the JSON primitive value "null", then the schema accepts the instance.¶
Otherwise:¶
If the instance is not accepted, then the error indicator for this case shall have an "instancePath" pointing to the instance and a "schemaPath" pointing to the schema member with the name "enum".¶
For example, the schema¶
{ "enum": ["PENDING", "DONE", "CANCELED"] }¶
accepts¶
"PENDING"¶
and¶
"DONE"¶
and¶
"CANCELED"¶
but rejects all of¶
0¶
and¶
1¶
and¶
2¶
and¶
"UNKNOWN"¶
and¶
null¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/enum" }]¶
The schema¶
{ "enum": ["PENDING", "DONE", "CANCELED"], "nullable": true }¶
accepts¶
"PENDING"¶
and¶
null¶
but rejects¶
1¶
and¶
"UNKNOWN"¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/enum" }]¶
The "elements" form is meant to describe instances that must be arrays. A further subschema describes the elements of the array. The syntax of the "elements" form is described in Section 2.2.5.¶
If a schema is of the "elements" form, then:¶
If the schema has a member named "nullable" whose value is the boolean "true", and the instance is the JSON primitive value "null", then the schema accepts the instance.¶
Otherwise:¶
Let S be the value of the schema member with the name "elements". The instance is accepted if and only if all of the following are true:¶
For example, the schema¶
{ "elements": { "type": "float32" } }¶
accepts¶
[]¶
and¶
[1, 2, 3]¶
but rejects¶
null¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/elements" }]¶
and rejects¶
[1, 2, "foo", 3, "bar"]¶
with the error indicators¶
[ { "instancePath": "/2", "schemaPath": "/elements/type" }, { "instancePath": "/4", "schemaPath": "/elements/type" } ]¶
The schema¶
{ "elements": { "type": "float32" }, "nullable": true }¶
accepts¶
null¶
and¶
[]¶
and¶
[1, 2, 3]¶
but rejects¶
[1, 2, "foo", 3, "bar"]¶
with the error indicators¶
[ { "instancePath": "/2", "schemaPath": "/elements/type" }, { "instancePath": "/4", "schemaPath": "/elements/type" } ]¶
The "properties" form is meant to describe JSON objects being used as a "struct". The syntax of the "properties" form is described in Section 2.2.6.¶
If a schema is of the "properties" form, then:¶
If the schema has a member named "nullable" whose value is the boolean "true", and the instance is the JSON primitive value "null", then the schema accepts the instance.¶
Otherwise:¶
The instance must be an object.¶
Otherwise, the schema rejects the instance. The error indicator for this case shall have an "instancePath" pointing to the instance, and a "schemaPath" pointing to the schema member with the name "properties" if such a schema member exists; if such a member doesn't exist, "schemaPath" shall point to the schema member with the name "optionalProperties".¶
If the instance is an object, and the schema has a member named "properties", then let P be the value of the schema member named "properties". Per Section 2.2.6, we know P is an object. For every member name in P, a member of the same name in the instance must exist.¶
Otherwise, the schema rejects the instance. The error indicator for this case shall have an "instancePath" pointing to the instance, and a "schemaPath" pointing to the member of P failing the requirement just described.¶
If the instance is an object, then let P be the value of the schema member named "properties" (if it exists) and O be the value of the schema member named "optionalProperties" (if it exists).¶
For every member I of the instance, find a member with the same name as I's in P or O. Per Section 2.2.6, we know it is not possible for both P and O to have such a member. If the "discriminator tag exemption" is in effect on I (see Section 3.3.8), then ignore I.¶
Otherwise:¶
If no such member in P or O exists and validation is not in "allow additional properties" mode (see Section 3.1), then the schema rejects the instance.¶
The error indicator for this case has an "instancePath" pointing to I and a "schemaPath" pointing to the schema.¶
If such a member in P or O does exist, then call this member S. If S rejects I's value, then the schema rejects the instance.¶
The error indicators for this case are the union of the error indicators from evaluating S against I's value.¶
If an instance is an object, it may have multiple errors arising from the second and third bullet in the list above. In this case, the error indicators are the union of the errors.¶
For example, the schema¶
{ "properties": { "a": { "type": "string" }, "b": { "type": "string" } }, "optionalProperties": { "c": { "type": "string" }, "d": { "type": "string" } } }¶
accepts¶
{ "a": "foo", "b": "bar" }¶
and¶
{ "a": "foo", "b": "bar", "c": "baz" }¶
and¶
{ "a": "foo", "b": "bar", "c": "baz", "d": "quux" }¶
and¶
{ "a": "foo", "b": "bar", "d": "quux" }¶
but rejects¶
null¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/properties" }]¶
and rejects¶
{ "b": 3, "c": 3, "e": 3 }¶
with the error indicators¶
[ { "instancePath": "", "schemaPath": "/properties/a" }, { "instancePath": "/b", "schemaPath": "/properties/b/type" }, { "instancePath": "/c", "schemaPath": "/optionalProperties/c/type" }, { "instancePath": "/e", "schemaPath": "" } ]¶
If instead the schema had "additionalProperties: true" but was otherwise the same:¶
{ "properties": { "a": { "type": "string" }, "b": { "type": "string" } }, "optionalProperties": { "c": { "type": "string" }, "d": { "type": "string" } }, "additionalProperties": true }¶
and the instance remained the same:¶
{ "b": 3, "c": 3, "e": 3 }¶
then the error indicators from evaluating the instance against the schema would be:¶
[ { "instancePath": "", "schemaPath": "/properties/a" }, { "instancePath": "/b", "schemaPath": "/properties/b/type" }, { "instancePath": "/c", "schemaPath": "/optionalProperties/c/type" }, ]¶
These are the same errors as before, except the final error (associated with the additional member named "e" in the instance) is no longer present. This is because "additionalProperties: true" enables "allow additional properties" mode on the schema.¶
Finally, the schema¶
{ "nullable": true, "properties": { "a": { "type": "string" }, "b": { "type": "string" } }, "optionalProperties": { "c": { "type": "string" }, "d": { "type": "string" } }, "additionalProperties": true }¶
accepts¶
null¶
but rejects¶
{ "b": 3, "c": 3, "e": 3 }¶
with the error indicators¶
[ { "instancePath": "", "schemaPath": "/properties/a" }, { "instancePath": "/b", "schemaPath": "/properties/b/type" }, { "instancePath": "/c", "schemaPath": "/optionalProperties/c/type" }, ]¶
The "values" form is meant to describe instances that are JSON objects being used as an associative array. The syntax of the "values" form is described in Section 2.2.7.¶
If a schema is of the "values" form, then:¶
If the schema has a member named "nullable" whose value is the boolean "true", and the instance is the JSON primitive value "null", then the schema accepts the instance.¶
Otherwise:¶
Let S be the value of the schema member with the name "values". The instance is accepted if and only if all of the following are true:¶
For example, the schema¶
{ "values": { "type": "float32" } }¶
accepts¶
{}¶
and¶
{"a": 1, "b": 2}¶
but rejects¶
null¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/values" }]¶
and rejects¶
{ "a": 1, "b": 2, "c": "foo", "d": 3, "e": "bar" }¶
with the error indicators¶
[ { "instancePath": "/c", "schemaPath": "/values/type" }, { "instancePath": "/e", "schemaPath": "/values/type" } ]¶
The schema¶
{ "nullable": true, "values": { "type": "float32" } }¶
accepts¶
null¶
but rejects¶
{ "a": 1, "b": 2, "c": "foo", "d": 3, "e": "bar" }¶
with the error indicators¶
[ { "instancePath": "/c", "schemaPath": "/values/type" }, { "instancePath": "/e", "schemaPath": "/values/type" } ]¶
The "discriminator" form is meant to describe JSON objects being used in a fashion similar to a discriminated union construct in C-like languages. The syntax of the "discriminator" form is described in Section 2.2.8.¶
When a schema is of the "discriminator" form, it validates that:¶
The behavior of the "discriminator" form is more complex than the other keywords. Readers familiar with CDDL may find the final example in Appendix B helpful in understanding its behavior. What follows in this section is a description of the "discriminator" form's behavior, as well as some examples.¶
If a schema is of the "discriminator" form, then:¶
If the schema has a member named "nullable" whose value is the boolean "true", and the instance is the JSON primitive value "null", then the schema accepts the instance. Otherwise, the instance is accepted if and only if all of the following are true:¶
The instance is an object.¶
Otherwise, the error indicator for this case shall have an "instancePath" pointing to the instance and a "schemaPath" pointing to D.¶
If the instance is a JSON object, then I must exist.¶
Otherwise, the error indicator for this case shall have an "instancePath" pointing to the instance and a "schemaPath" pointing to D.¶
If the instance is a JSON object and I exists, I's value must be a string.¶
Otherwise, the error indicator for this case shall have an "instancePath" pointing to I and a "schemaPath" pointing to D.¶
If the instance is a JSON object and I exists and has a string value, then S must exist.¶
Otherwise, the error indicator for this case shall have an "instancePath" pointing to I and a "schemaPath" pointing to M.¶
If the instance is a JSON object, I exists, and S exists, then the instance must satisfy S's value. Per Section 2, we know S's value is a schema of the "properties" form. Apply the "discriminator tag exemption" afforded in Section 3.3.6 to I when evaluating whether the instance satisfies S's value.¶
Otherwise, the error indicators for this case shall be error indicators from evaluating S's value against the instance, with the "discriminator tag exemption" applied to I.¶
The list items above are defined in a mutually exclusive way. For any given instance and schema, exactly one of the list items above will apply.¶
For example, the schema¶
{ "discriminator": "version", "mapping": { "v1": { "properties": { "a": { "type": "float32" } } }, "v2": { "properties": { "a": { "type": "string" } } } } }¶
rejects¶
null¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/discriminator" }]¶
(This is the case of the instance not being an object.)¶
Also rejected is¶
{}¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/discriminator" }]¶
(This is the case of I not existing.)¶
Also rejected is¶
{ "version": 1 }¶
with the error indicator¶
[ { "instancePath": "/version", "schemaPath": "/discriminator" } ]¶
(This is the case of I existing but not having a string value.)¶
Also rejected is¶
{ "version": "v3" }¶
with the error indicator¶
[ { "instancePath": "/version", "schemaPath": "/mapping" } ]¶
(This is the case of I existing and having a string value but S not existing.)¶
Also rejected is¶
{ "version": "v2", "a": 3 }¶
with the error indicator¶
[ { "instancePath": "/a", "schemaPath": "/mapping/v2/properties/a/type" } ]¶
(This is the case of I and S existing but the instance not satisfying S's value.)¶
Finally, the schema accepts¶
{ "version": "v2", "a": "foo" }¶
This instance is accepted even though "version" is not mentioned by "/mapping/v2/properties"; the "discriminator tag exemption" ensures that "version" is not treated as an additional property when evaluating the instance against S's value.¶
By contrast, consider the same schema but with "nullable" being "true". The schema¶
{ "nullable": true, "discriminator": "version", "mapping": { "v1": { "properties": { "a": { "type": "float32" } } }, "v2": { "properties": { "a": { "type": "string" } } } } }¶
accepts¶
null¶
To further illustrate the "discriminator" form with examples, recall the JTD schema in Section 2.2.8, reproduced here:¶
{ "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" } } } } }¶
This schema accepts¶
{ "event_type": "account_deleted", "account_id": "abc-123" }¶
and¶
{ "event_type": "account_payment_plan_changed", "account_id": "abc-123", "payment_plan": "PAID" }¶
and¶
{ "event_type": "account_payment_plan_changed", "account_id": "abc-123", "payment_plan": "PAID", "upgraded_by": "users/mkhwarizmi" }¶
but rejects¶
{}¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/discriminator" }]¶
and rejects¶
{ "event_type": "some_other_event_type" }¶
with the error indicator¶
[ { "instancePath": "/event_type", "schemaPath": "/mapping" } ]¶
and rejects¶
{ "event_type": "account_deleted" }¶
with the error indicator¶
[{ "instancePath": "", "schemaPath": "/mapping/account_deleted/properties/account_id" }]¶
and rejects¶
{ "event_type": "account_payment_plan_changed", "account_id": "abc-123", "payment_plan": "PAID", "xxx": "asdf" }¶
with the error indicator¶
[{ "instancePath": "/xxx", "schemaPath": "/mapping/account_payment_plan_changed" }]¶
This document has no IANA actions.¶
Implementations of JTD will necessarily be manipulating JSON data. Therefore, the security considerations of [RFC8259] are all relevant here.¶
Implementations that evaluate user-inputted schemas SHOULD implement mechanisms to detect and abort circular references that might cause a naive implementation to go into an infinite loop. Without such mechanisms, implementations may be vulnerable to denial-of-service attacks.¶
This appendix is not normative.¶
This section describes possible features that are intentionally left out of JSON Type Definition and justifies why these features are omitted.¶
This document does not allow "int64" or "uint64" as values for the JTD "type" keyword (see Sections 2.2.3 and 3.3.3). Such hypothetical "int64" or "uint64" types would behave like "int32" or "uint32" (respectively) but with the range of values associated with 64-bit instead of 32-bit integers. That is:¶
Users of "int64" and "uint64" would likely expect that the full range of signed or unsigned 64-bit integers could interoperably be transmitted as JSON without loss of precision. But this assumption is likely to be incorrect, for the reasons given in Section 2.2 of [RFC7493].¶
"int64" and "uint64" likely would have led users to falsely assume that the full range of 64-bit integers can be interoperably processed as JSON without loss of precision. To avoid leading users astray, JTD omits "int64" and "uint64".¶
This document disallows the "definitions" keyword from appearing outside of root schemas (see Figure 1). Conceivably, this document could have instead allowed "definitions" to appear on any schema, even non-root ones. Under this alternative design, "ref"s would resolve to a definition in the "nearest" (i.e., most nested) schema that both contained the "ref" and had a suitably named "definitions" member.¶
For instance, under this alternative approach, one could define schemas like the one in Figure 3.¶
If schemas like that in Figure 3 were permitted, code generation from JTD schemas would be more difficult, and the generated code would be less useful.¶
Code generation would be more difficult because it would force code generators to implement a name-mangling scheme for types generated from definitions. This additional difficulty is not immense, but it adds complexity to an otherwise relatively trivial task.¶
Generated code would be less useful because generated, mangled struct names are less pithy than human-defined struct names. For instance, the "user" definitions in Figure 3 might have been generated into types named "PropertiesFooUser", "PropertiesBarUser", and "PropertiesBazUser"; obtuse names like these are less useful to human-written code than names like "User".¶
Furthermore, even though "PropertiesFooUser" and "PropertiesBarUser" would be essentially identical, they would not be interchangeable in many statically typed programming languages. A code generator could attempt to circumvent this by deduplicating identical definitions, but then the user might be confused as to why the subtly distinct "PropertiesBazUser", defined from a schema allowing a property named "userId" (not "user_id"), was not deduplicated.¶
Because there seem to be implementation and usability challenges associated with non-root definitions, and because it would be easier to later amend JTD to permit for non-root definitions than to later amend JTD to prohibit them, this document does not permit non-root definitions in JTD schemas.¶
This appendix is not normative.¶
To aid the reader familiar with CDDL, this section illustrates how JTD works by presenting JTD schemas and CDDL schemas that accept and reject the same instances.¶
The JTD schema¶
{}¶
accepts the same instances as the CDDL rule¶
root = any¶
The JTD schema¶
{ "definitions": { "a": { "elements": { "ref": "b" }}, "b": { "type": "float32" } }, "elements": { "ref": "a" } }¶
accepts the same instances as the CDDL rule¶
root = [* a] a = [* b] b = number¶
The JTD schema¶
{ "enum": ["PENDING", "DONE", "CANCELED"]}¶
accepts the same instances as the CDDL rule¶
root = "PENDING" / "DONE" / "CANCELED"¶
The JTD schema¶
{"type": "boolean"}¶
accepts the same instances as the CDDL rule¶
root = bool¶
The JTD schemas:¶
{"type": "float32"}¶
and¶
{"type": "float64"}¶
both accept the same instances as the CDDL rule¶
root = number¶
The JTD schema¶
{"type": "string"}¶
accepts the same instances as the CDDL rule¶
root = tstr¶
The JTD schema¶
{"type": "timestamp"}¶
accepts the same instances as the CDDL rule¶
root = tdate¶
The JTD schema¶
{ "elements": { "type": "float32" }}¶
accepts the same instances as the CDDL rule¶
root = [* number]¶
The JTD schema¶
{ "properties": { "a": { "type": "boolean" }, "b": { "type": "float32" } }, "optionalProperties": { "c": { "type": "string" }, "d": { "type": "timestamp" } } }¶
accepts the same instances as the CDDL rule¶
root = { a: bool, b: number, ? c: tstr, ? d: tdate }¶
The JTD schema¶
{ "values": { "type": "float32" }}¶
accepts the same instances as the CDDL rule¶
root = { * tstr => number }¶
Finally, the JTD schema¶
{ "discriminator": "a", "mapping": { "foo": { "properties": { "b": { "type": "float32" } } }, "bar": { "properties": { "b": { "type": "string" } } } } }¶
accepts the same instances as the CDDL rule¶
root = { a: "foo", b: number } / { a: "bar", b: tstr }¶
This appendix is not normative.¶
As a demonstration of JTD, in Figure 4 is a JTD schema closely equivalent to the plain-English definition "reputation-object" described in Section 6.2.2 of [RFC7071]:¶
This schema does not enforce the requirement that "sample-size", "generated", and "expires" be unbounded positive integers. It does not express the limitation that "rating", "confidence", and "normal-rating" should not have more than three decimal places of precision.¶
The example in Figure 4 can be compared against the equivalent example in Appendix H of [RFC8610].¶
Carsten Bormann provided lots of useful guidance and feedback on JTD's design and the structure of this document.¶
Evgeny Poberezkin suggested the addition of "nullable" and thoroughly vetted this document for mistakes and opportunities for simplification.¶
Tim Bray suggested the current "ref" model and the addition of "enum". Anders Rundgren suggested extending "type" to have more support for numerical types. James Manger suggested additional clarifying examples of how integer types work. Adrian Farrel suggested many improvements to help make this document clearer.¶
Members of the IETF JSON mailing list -- in particular, Pete Cordell, Phillip Hallam-Baker, Nico Williams, John Cowan, Rob Sayre, and Erik Wilde -- provided lots of useful feedback.¶
OpenAPI's "discriminator" object [OPENAPI] inspired the "discriminator" form. [JSON-SCHEMA] influenced various parts of JTD's early design.¶