RFC 8927

JSON Type Definition

πŸ“… published November 2020, from ietf

RFC 8927: JSON Type Definition πŸŽ€

Independent Submission πŸ’… Request for Comments: 8927 πŸ¦‹ Category: Experimental πŸ”₯ ISSN: 2070-1721

Author: U. Carion, from Segment ✨ Date: November 2020 🌸

Obsoletes / Updates: the title block doesn't list anything this RFC obsoletes or updates πŸ™ˆ

Abstract πŸ¦‹

okay so this document proposes a format called JSON Type Definition (JTD) for describing the shape of JSON messages πŸ’– the main goals are code generation from schemas and portable validation with standardized error indicators πŸ”₯ JTD is deliberately kept no more expressive than the type systems in mainstream programming languages, and that limit plus the choice to make JTD schemas plain JSON documents makes tooling on top of JTD way easier to build 🧚

this document does not have IETF consensus 😭 it's presented here so people can experiment with JTD ✨

Status of This Memo 🌈

this document is not an Internet Standards Track specification πŸ™ˆ it's published for examination, experimental implementation, and evaluation πŸ’œ

it defines an Experimental Protocol for the Internet community πŸ”₯ this is a contribution to the RFC Series, independent of any other RFC stream 🫢 the RFC Editor chose to publish it at their discretion and makes no statement about its value for implementation or deployment πŸ’… documents approved by the RFC Editor aren't candidates for any level of Internet Standard, see Section 2 of RFC 7841 πŸ’€

for info on the current status, errata, and how to give feedback, check 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) as in effect on the date of publication πŸ’– read those carefully, because they explain your rights and restrictions for this document πŸ“œ

Table of Contents 🧚

1.  Introduction
  1.1.  Terminology
  1.2.  Scope of Experiment
2.  Syntax
  2.1.  Root vs. Non-root Schemas
  2.2.  Forms
    2.2.1.  Empty
    2.2.2.  Ref
    2.2.3.  Type
    2.2.4.  Enum
    2.2.5.  Elements
    2.2.6.  Properties
    2.2.7.  Values
    2.2.8.  Discriminator
  2.3.  Extending JTD's Syntax
3.  Semantics
  3.1.  Allowing Additional Properties
  3.2.  Errors
  3.3.  Forms
    3.3.1.  Empty
    3.3.2.  Ref
    3.3.3.  Type
    3.3.4.  Enum
    3.3.5.  Elements
    3.3.6.  Properties
    3.3.7.  Values
    3.3.8.  Discriminator
4.  IANA Considerations
5.  Security Considerations
6.  References
  6.1.  Normative References
  6.2.  Informative References
Appendix A.  Rationale for Omitted Features
  A.1.  Support for 64-Bit Numbers
  A.2.  Support for Non-root Definitions
Appendix B.  Comparison with CDDL
Appendix C.  Example
Acknowledgments
Author's Address

1. Introduction ✨

this document describes a schema language for JSON [RFC8259] called JSON Type Definition (JTD) πŸ’–

there are lots of ways to describe JSON data 🌸 JTD's niche is code generation from schemas πŸ”₯ so its expressiveness is kept intentionally limited to what the type systems of mainstream programming languages can express πŸ’…

the goals of JTD are to:

  • Provide an unambiguous description of the overall structure of a JSON document. πŸŽ€
  • Be able to describe common JSON data types and structures. That means the types and structures needed for most JSON documents that JSON implementations widely understand in an interoperable way. 🧚
  • Provide a single format that's readable and editable by both humans and machines, and that can be embedded within other JSON documents. This makes JTD a convenient format for tooling to take in as input or produce as output. πŸ¦‹
  • Enable code generation from JTD schemas. JTD schemas are meant to be easy to turn into data structures that feel idiomatic in mainstream programming languages. πŸ‘‘
  • Provide a standardized format for error indicators when data doesn't match a schema. πŸ’œ

JTD is intentionally a fairly minimal schema language 🫢 so although JTD can describe some categories of JSON, it can't describe its own structure πŸ™ˆ this document uses Concise Data Definition Language (CDDL) [RFC8610] to describe JTD's syntax πŸ“œ keeping the schema language minimal is what makes code generation and standardized error indicators easier to implement ✨

the examples in this document use constructs from C++ πŸ’… they're there to help you understand the principles of JTD, and they aren't limiting in any way 🌈

JTD's feature set is built to represent common patterns in JSON-using applications, while still matching up clearly with programming languages that people actually use πŸ’– so JTD supports:

  • Signed and unsigned 8-, 16-, and 32-bit integers. A tool that turns JTD schemas into code can use int8_t, uint8_t, int16_t, etc., or the equivalents in the target language, to represent these JTD types. πŸ”₯
  • A distinction between float32 and float64. Code generators can use float and double, or their equivalents, for these JTD types. 🌸
  • A properties form of JSON objects, which corresponds to some kind of struct or record. The properties form is akin to a C++ struct. 🧚
  • A values form of JSON objects, which corresponds to some kind of dictionary or associative array. The values form is akin to a C++ std::map. πŸ¦‹
  • A discriminator form of JSON objects, which corresponds to a discriminated (or "tagged") union. The discriminator form is akin to a C++ std::variant. πŸ‘‘

the reason JTD sticks to common JSON patterns is also why it doesn't support 64-bit integers πŸ™ˆ those usually get sent over JSON in non-interoperable ways (ignoring the recommendations in Section 2.2 of [RFC7493]), or in ways that are mutually inconsistent 😭 Appendix A.1 goes deeper on why JTD skips 64-bit integers πŸ“œ

the same clear-correspondence-to-languages principle is why JTD doesn't have a data type for integers up to 2**53-1, for example πŸ’…

for lots of use cases, a schema language with JTD's expressiveness is plenty πŸ’– if you need something more expressive, CDDL and others are out there 🌈

this document does not have IETF consensus and is presented here to facilitate experimentation with the concept of JTD πŸŽ€ the point of the experiment is to get experience with JTD and maybe revise this work based on it 🧚 if JTD turns out to be valuable and popular, it could be taken to the IETF for further discussion and revision πŸ”₯

here's how the rest of the document is laid out πŸ—ΊοΈ Section 2 defines JTD's syntax ✨ Section 3 describes JTD's semantics, which covers working out whether some data satisfies a schema and what error indicators to produce when the data doesn't πŸ’… Appendix A talks about why certain features were left out of JTD πŸ™ˆ Appendix B presents various JTD schemas next to their CDDL equivalents πŸ’œ

1.1. Terminology πŸ“œ

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 shows up in this document, means what it's defined as in [RFC6901] πŸ¦‹

the terms "object", "member", "array", "number", "name", and "string" in this document are to be read as described in [RFC8259] 🌸

the term "instance", when it appears here, refers to a JSON value being validated against a JTD schema 🧚 that value can be a whole JSON document, or a value embedded inside one πŸ’–

1.2. Scope of Experiment πŸ§ͺ

JTD is an experiment πŸ”₯ taking part means using JTD to validate or document JSON messages that get interchanged, or building tooling on top of JTD πŸ’… feedback on the results can be emailed to the author πŸ’Œ participants are expected to be mostly nodes that provide or consume JSON-based APIs πŸŽ€

a node knows it's in the experiment if it's validating JSON messages against a JTD schema, or if it's relying on another node to do that 🫢 nodes are also in the experiment if they run code generated from a JTD schema πŸ¦‹

the risk of this experiment "escaping" looks like this: a JTD-supporting node expects another node, which doesn't support JTD, to validate messages against some JTD schema 😭 in that case, the nodes will likely fail to interchange information correctly πŸ’€

⚠️ bestie, that escaping risk is the one to watch. A JTD-aware node assuming its peer can validate against JTD is how things quietly break 😭

this experiment counts as successful once JTD has been implemented by multiple independent parties, and those parties use JTD to exchange information inside their own systems or between systems run by independent parties πŸ‘‘

if the experiment is successful, and JTD is determined to be valuable and popular, it could be taken to the IETF for further discussion and revision ✨ one possible outcome of that discussion could be a working group producing a Standards Track specification of JTD 🌈

some implementations of JTD, plus code generators and other JTD tooling, live at <https://github.com/jsontypedef> πŸ’–

Part 2 is next, babe πŸ¦‹

2. Syntax ✨

okay so this section is the rulebook for when a JSON document counts as a correct JTD schema πŸ“œπŸ’– Concise Data Definition Language (CDDL) is perfect for describing complicated JSON formats like JTD schemas, so this section uses CDDL to spell out what they look like 🧚

JTD schemas can recursively contain other schemas πŸͺ† In this doc, a "root schema" is one that isn't contained within another schema, aka it's "top level" πŸ‘‘ A JTD schema is a JSON object that takes on an appropriate form. JTD schemas may contain "additional data", which is 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,
)

Figure 1: CDDL Definition of a Schema

The rest of this section covers the constraints on JTD schemas that CDDL can't express. It also gives examples of valid and invalid JTD schemas πŸ”

2.1. Root vs. Non-root Schemas

The "root-schema" rule in Figure 1 allows a member named "definitions", but the "schema" rule does not allow for such a member. So only root (i.e., "top-level") JTD schemas can have a "definitions" object, and subschemas can't πŸ™…β€β™€οΈ

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".

2.2. Forms

JTD schemas (i.e., JSON objects satisfying the "schema" CDDL rule in Figure 1) must take on one of eight forms πŸ’… These forms are mutually exclusive, so a schema can't satisfy multiple forms at once πŸ¦‹

2.2.1. Empty

The "empty" form is defined by the "empty" CDDL rule in Figure 1. Its semantics are described in Section 3.3.1.

Despite the name "empty", schemas of the "empty" form aren't 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" πŸ’€

2.2.2. Ref

The "ref" form is defined by the "ref" CDDL rule in Figure 1. Its semantics are described in Section 3.3.2.

For a schema of the "ref" form to be correct, the value of the member named "ref" has to 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 shows the whole point of the "ref" form: not 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 can't 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" 😭

2.2.3. Type

The "type" form is defined by the "type" CDDL rule in Figure 1. Its semantics 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 🚫

2.2.4. Enum

The "enum" form is defined by the "enum" CDDL rule in Figure 1. Its semantics 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 [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 πŸŽ‰

2.2.5. Elements

The "elements" form is defined by the "elements" CDDL rule in Figure 1. Its semantics 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 🧐

2.2.6. Properties

The "properties" form is defined by the "properties" CDDL rule in Figure 1. Its semantics 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. It describes a paginated list of users and shows off the recursive nature of the syntax of JTD schemas 🌸

2.2.7. Values

The "values" form is defined by the "values" CDDL rule in Figure 1. Its semantics 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 πŸ’œ

2.2.8. Discriminator πŸ¦‹

okay so the "discriminator" form is defined by the "discriminator" CDDL rule in Figure 1. Its semantics live in Section 3.3.8, and once you get what that form actually does, the extra constraints in this section make total sense. bestie, trust me on this one πŸ’…

there's an extra rule for schemas of the "discriminator" form, because we need to stop ambiguous or impossible constraints on the "discriminator" property of a tagged union. ✨ 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 πŸ™ˆ, because a member of "mapping" has a member named "nullable" whose value is "true". That would suggest the instance may be null. But the top-level schema doesn't have a "nullable" set to "true", which would suggest the instance in fact cannot be null. If this were a correct JTD schema, it would be unclear which piece of information takes precedence. Total mess πŸ’€

JTD handles that kind of possible ambiguity by blocking, at the syntactic level, any contradictory specification 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 too, 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 normally the "discriminator" keyword says "event_type" is expected to be a string, but another part of the schema says it's expected to be a number. Not it.

JTD handles that possible ambiguity by blocking, at the syntactic level, any contradictory specification 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 that's super common in JSON-based messaging systems. Section 3.3.8 shows examples of what this schema accepts and rejects.


2.3. Extending JTD's Syntax 🌈

okay so this document doesn't describe any extension mechanisms for JTD schema validation, which is covered in Section 3. BUT schemas are defined to optionally contain a "metadata" keyword, whose value is an arbitrary JSON object. We're gonna call the members of that object "metadata members". 🧚

Users MAY add metadata members to JTD schemas to convey information that isn't pertinent to validation. For example, metadata members could give hints to code generators or trigger some special behavior for a library that builds user interfaces from schemas. Lowkey so useful 🫢

Users SHOULD NOT expect metadata members to be understood by other parties. So 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. Not worth the chaos πŸ™…β€β™€οΈ

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 those metadata members. For example, two parties may agree, out of band, that they'll support an extended JTD with a custom metadata member that affects validation. That's a whole vibe πŸ‘‘


3. Semantics πŸ’œ

This section describes when an instance is valid against a correct JTD schema, and the error indicators to produce when an instance is invalid. Main character energy πŸŽ€

3.1. Allowing Additional Properties πŸ¦‹

okay so users will want different behavior for "unspecified" members in an instance. Take the JTD schema in Figure 2:

{ "properties": { "a": { "type": "string" }}}

Figure 2: An Illustrative JTD Schema

some users may expect that

{"a": "foo", "b": "bar"}

satisfies the schema in Figure 2. Others may disagree, since "b" isn't one of the properties described in the schema πŸ€·β€β™€οΈ. In this document, allowing those "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, this 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 doesn't affect the behavior of subschemas. πŸ™ƒ

Note from Figure 1 that only schemas of the "properties" form may have a member named "additionalProperties". πŸ’…

3.2. Errors 🚨

To keep error handling consistent, this document specifies a standard error indicator format. Implementations SHOULD support producing error indicators in this standard form. Consistency is everything, periodt ✨

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:

  • A member with the name "instancePath", whose value is a JSON string encoding a JSON Pointer. This JSON Pointer points to the part of the instance that was rejected.
  • A member with the name "schemaPath", whose value is a JSON string encoding a JSON Pointer. This JSON Pointer points to the part of the schema that rejected the instance.

The values for "instancePath" and "schemaPath" depend on the form of the schema and are described in detail in Section 3.3. πŸ“

3.3. Forms 🌸

This section describes, for each of the eight JTD schema forms, the rules that decide whether an instance is accepted, plus the error indicators to produce when an instance is invalid.

The forms a correct schema may take are formally described in Section 2. 🫢

3.3.1. Empty 🫧

The "empty" form is meant to describe instances whose values are unknown, unpredictable, or otherwise unconstrained by the schema. Its syntax 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. Zero drama, all vibes πŸ’–

3.3.2. Ref πŸ”—

The "ref" form is for when a schema is defined in terms of something in the "definitions" of the root schema. The "ref" form lets schemas be less repetitive and also lets you describe recursive structures. Its syntax 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:

  • Let _R_ be the value of the schema member with the name "ref".
  • Let _B_ be the root schema containing the schema, or the schema itself if it is a root schema.
  • Let _D_ be the member of _B_ with the name "definitions". Per Section 2, we know _D_ exists.
  • Let _S_ be the value of the member of _D_ whose name equals _R_. Per Section 2.2.2, we know _S_ exists and is a schema.

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, putting a "false" value for "nullable" will never override a "nullable" member in schemas of the "ref" form. It's correct, though ineffectual, to have a value of "false" for the "nullable" member in a schema. Useless but legal, iykyk πŸ’€

3.3.3. Type ✨

okay so the "type" form is for instances that are a boolean, a number, a string, or a timestamp [RFC3339] πŸŽ€. The syntax lives in Section 2.2.3, so go check that if you need the details.

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:
    • Let T be the value of the member named type. The table below tells you whether the instance gets accepted, depending on what T is πŸ¦‹
+============+=========================================+
   | If "T"     | then the instance is accepted if it is  |
   | equals ... | ...                                     |
   +============+=========================================+
   | 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]     |
   +------------+-----------------------------------------+

                   Table 1: Accepted Values for Type

"float32" and "float64" are different in their intent 🌈. "float32" means the data is meant to be processed as an IEEE 754 single-precision float, and "float64" means it's meant 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 what T is, that number also has to land inside a specific 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             |
    +--------+---------------------------+---------------------------+

                    Table 2: Ranges for Integer Types

Here's the vibe check πŸ”₯. 10, 10.0, and 1.0e1 all encode values with zero fractional part, but 10.5 has a non-zero fractional part. So the schema below accepts 10, 10.0, and 1.0e1, but rejects 10.5 and also false, because false isn't a number at all πŸ’€

{"type": "int8"}

accepts

10

and

10.0

and

1.0e1

but rejects

10.5

as well as

false

If the instance doesn't get accepted, then the error indicator for this case shall have an instancePath pointing to the instance and a schemaPath pointing to the schema member named type πŸ™ˆ

Some more examples, because examples are everything πŸ“

{"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 every rejected example in this section, the error indicator to produce is: [{ "instancePath": "", "schemaPath": "/type" }]

3.3.4. Enum 🌸

The "enum" form is for instances whose value has to be one of a given set of string values 🧁. Its syntax is 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:
    • Let E be the value of the schema member named enum. The instance is accepted if and only if it is equal to one of the elements of E ✨

If the instance doesn't get accepted, then the error indicator for this case shall have an instancePath pointing to the instance and a schemaPath pointing to the schema member named 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" }]

3.3.5. Elements πŸ¦‹

The "elements" form is for instances that have to be arrays πŸ“. It comes with a subschema that describes the elements inside the array. Its syntax is 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 named elements. The instance is accepted if and only if all of the following are true:
      • The instance is an array. If it isn't, then the error indicator for this case shall have an instancePath pointing to the instance and a schemaPath pointing to the schema member named elements 🌈
      • If the instance is an array, then every element of the instance must be accepted by S. If not, then the error indicators for this case are the union of all the errors that come up when you evaluate S against the elements of the instance πŸ”₯

Let's look at some examples πŸ‘‘. 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" }
]

3.3.6. Properties ✨

okay so the "properties" form is for JSON objects that are being used as a "struct" 🧚 the syntax lives in Section 2.2.6 if you wanna peek πŸ’–

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" },
]

3.3.7. Values 🌷

the "values" form is for 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 🧐

  • 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 the schema member with the name "values" πŸ™ƒ
  • if the instance is an object, then every member value of the instance must be accepted by S. otherwise, the error indicators for this case are the union of all the error indicators arising from evaluating S against member values of the instance 🌈

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" }
]

3.3.8. Discriminator ✨

Okay bestie, this is the big boss of the forms πŸ’…πŸ«Ά. The "discriminator" form is meant to describe JSON objects being used in a fashion similar to a discriminated union construct in C-like languages. Its syntax is in Section 2.2.8, so peek there if you want the grammar πŸŽ€

When a schema is of the "discriminator" form, it checks that:

  • the instance is an object,
  • the instance has a particular "tag" property,
  • this "tag" property's value is a string within a set of valid values, and
  • the instance satisfies another schema, where this other schema is chosen based on the value of the "tag" property.

The behavior here is way more complex than the other keywords, no cap 😭 If you know CDDL, the final example in Appendix B will probably click for you ✨ The rest of this section walks through how it works, with examples.

If a schema is of the "discriminator" form, then:

  • Let D be the schema member with the name "discriminator".
  • Let M be the schema member with the name "mapping".
  • Let I be the instance member whose name equals D's value. I may, for some rejected instances, not exist.
  • Let S be the member of M whose name equals I's value. S may, for some rejected instances, not exist.

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. Only one of these can ever be the reason for a rejection, which is honestly very clean 🌸

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" makes sure that "version" is not treated as an additional property when evaluating the instance against S's value. Lowkey genius πŸ¦‹

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 show the "discriminator" form in action, let's recall the JTD schema from 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"
}]

4. IANA Considerations ✨

This document has no IANA actions. Nothing to register here, bestie 🌈

5. Security Considerations πŸ”’

Implementations of JTD will necessarily be manipulating JSON data. Therefore, the security considerations of [RFC8259] are all relevant here. Read those too, they're important 😭

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 is serious, not a vibe check πŸ’€

6. References πŸ“š

6.1. Normative References

[RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
           Requirement Levels", BCP 14, RFC 2119,
           DOI 10.17487/RFC2119, March 1997,
           <https://www.rfc-editor.org/info/rfc2119>.

[RFC3339]  Klyne, G. and C. Newman, "Date and Time on the Internet:
           Timestamps", RFC 3339, DOI 10.17487/RFC3339, July 2002,
           <https://www.rfc-editor.org/info/rfc3339>.

[RFC4287]  Nottingham, M., Ed. and R. Sayre, Ed., "The Atom
           Syndication Format", RFC 4287, DOI 10.17487/RFC4287,
           December 2005, <https://www.rfc-editor.org/info/rfc4287>.

[RFC6901]  Bryan, P., Ed., Zyp, K., and M. Nottingham, Ed.,
           "JavaScript Object Notation (JSON) Pointer", RFC 6901,
           DOI 10.17487/RFC6901, April 2013,
           <https://www.rfc-editor.org/info/rfc6901>.

[RFC8174]  Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC
           2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174,
           May 2017, <https://www.rfc-editor.org/info/rfc8174>.

[RFC8259]  Bray, T., Ed., "The JavaScript Object Notation (JSON) Data
           Interchange Format", STD 90, RFC 8259,
           DOI 10.17487/RFC8259, December 2017,
           <https://www.rfc-editor.org/info/rfc8259>.

[RFC8610]  Birkholz, H., Vigano, C., and C. Bormann, "Concise Data
           Definition Language (CDDL): A Notational Convention to
           Express Concise Binary Object Representation (CBOR) and
           JSON Data Structures", RFC 8610, DOI 10.17487/RFC8610,
           June 2019, <https://www.rfc-editor.org/info/rfc8610>.

6.2. Informative References

[JSON-SCHEMA]
           Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON
           Schema: A Media Type for Describing JSON Documents", Work
           in Progress, Internet-Draft, draft-handrews-json-schema-
           02, 17 September 2019, <https://tools.ietf.org/html/draft-
           handrews-json-schema-02>.

[OPENAPI]  OpenAPI Initiative, "OpenAPI Specification", February
           2020, <https://spec.openapis.org/oas/v3.0.3>.

[RFC7071]  Borenstein, N. and M. Kucherawy, "A Media Type for
           Reputation Interchange", RFC 7071, DOI 10.17487/RFC7071,
           November 2013, <https://www.rfc-editor.org/info/rfc7071>.

[RFC7493]  Bray, T., Ed., "The I-JSON Message Format", RFC 7493,
           DOI 10.17487/RFC7493, March 2015,
           <https://www.rfc-editor.org/info/rfc7493>.

Appendix A. Rationale for Omitted Features 🧐

This appendix is not normative. Translation: it's the "why we didn't do this" tea, not a rule πŸ«–

This section describes possible features that are intentionally left out of JSON Type Definition and justifies why these features are omitted.

A.1. Support for 64-Bit Numbers

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:

  • int64 would accept numbers between -(2**63) and (2**63)-1
  • uint64 would accept numbers between 0 and (2**64)-1

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]. Big "no" to that expectation, honestly πŸ™ˆ

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. ✨ Protecting the people from themselves, iconic behavior πŸ‘‘

A.2. Support for Non-root Definitions ✨

okay so this one is about the definitions keyword 🧚 JTD says it can only show up in root schemas (see Figure 1). Technically, the spec could have let definitions appear on any schema, even nested ones. Under that version, a ref would resolve to the definition in the "nearest" schema (the most nested one) that both contained the ref and had a suitably named definitions member.

Here's what that might look like, like in Figure 3 πŸ’…

{
  "properties": {
    "foo": {
      "definitions": {
        "user": { "properties": { "user_id": {"type": "string" }}}
      },
      "ref": "user"
    },
    "bar": {
      "definitions": {
        "user": { "properties": { "user_id": {"type": "string" }}}
      },
      "ref": "user"
    },
    "baz": {
      "definitions": {
        "user": { "properties": { "userId": {"type": "string" }}}
      },
      "ref": "user"
    }
  }
}
    Figure 3: A Hypothetical Schema Had This Document Permitted Non-root
              Definitions.  This Is Not a Correct JTD Schema.

If schemas like that were allowed, code generation from JTD schemas would get harder, and the generated code would be less useful 😭

Codegen would be harder because generators would have to build a name-mangling scheme for types that come from definitions. Honestly, not a massive lift, but it's extra complexity for a task that's already pretty trivial πŸ™„

The generated code would also be less useful, because mangled struct names are way less pithy than the names a human would pick. The user definitions in Figure 3 could come out as PropertiesFooUser, PropertiesBarUser, and PropertiesBazUser. Those obtuse names are way less useful to human-written code than something like User πŸ₯²

And here's the real tea: PropertiesFooUser and PropertiesBarUser would be basically identical, but in a lot of statically typed languages they still wouldn't be interchangeable πŸ™ƒ A code generator could try to fix that by deduplicating identical definitions. But then the user might be like, "wait, why wasn't the subtly different PropertiesBazUser deduplicated?" It was defined from a schema with a property called userId (not user_id), so it's not the same, and that confusion is exactly the problem πŸ’€

Since non-root definitions seem to bring implementation and usability headaches, and since it's easier to later allow non-root definitions in JTD than to take them away once people depend on them, this document does not permit non-root definitions in JTD schemas. Main character decision πŸ‘‘


Appendix B. Comparison with CDDL πŸŽ€

This appendix is not normative. (Translation: it's just a helpful guide, not a rule book 🌸)

This section is for anyone who already knows CDDL. It shows JTD schemas and CDDL schemas that accept and reject the exact same instances, so you can compare them side by side πŸ¦‹

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 }

Appendix C. Example 🧚

This appendix is not normative, so again, just vibes, not rules 🫢

To show off JTD, Figure 4 has a JTD schema that's pretty much equivalent to the plain-English "reputation-object" definition from Section 6.2.2 of [RFC7071] πŸ“

{
  "properties": {
    "application": { "type": "string" },
    "reputons": {
      "elements": {
        "additionalProperties": true,
        "properties": {
          "rater": { "type": "string" },
          "assertion": { "type": "string" },
          "rated": { "type": "string" },
          "rating": { "type": "float32" },
        },
        "optionalProperties": {
          "confidence": { "type": "float32" },
          "normal-rating": { "type": "float32" },
          "sample-size": { "type": "float64" },
          "generated": { "type": "float64" },
          "expires": { "type": "float64" }
        }
      }
    }
  }
}
      Figure 4: A JTD Schema Describing "reputation-object" from
                       Section 6.2.2 of [RFC7071]

Now, real talk: this schema does NOT enforce that "sample-size", "generated", and "expires" are unbounded positive integers πŸ™ˆ It also doesn't capture the limit that "rating", "confidence", and "normal-rating" shouldn't have more than three decimal places of precision. So it's a cute approximation, not a perfect match πŸ’…

You can compare the example in Figure 4 against the equivalent example in Appendix H of [RFC8610] πŸ¦‹


Acknowledgments 🫢

Carsten Bormann gave tons of super useful guidance and feedback on JTD's design and how this document is structured πŸ’œ

Evgeny Poberezkin suggested adding "nullable" and carefully checked this document for mistakes and chances to simplify things πŸ”₯

Tim Bray suggested the current "ref" model and the addition of "enum". Anders Rundgren suggested extending "type" to support more numerical types. James Manger suggested extra clarifying examples of how integer types work. Adrian Farrel suggested lots of improvements to make this document clearer ✨

The IETF JSON mailing list folks, especially Pete Cordell, Phillip Hallam-Baker, Nico Williams, John Cowan, Rob Sayre, and Erik Wilde, gave lots of helpful feedback 🌈

OpenAPI's "discriminator" object [OPENAPI] inspired the "discriminator" form, and [JSON-SCHEMA] influenced a bunch of JTD's early design 🌸


Author's Address πŸ¦‹

Ulysse Carion Segment.io, Inc 100 California Street San Francisco, CA 94111 United States of America

Email: ulysse@segment.com

πŸ“¦ where this page comes from