Independent Submission                                         U. Carion
Request for Comments: 8927                                       Segment
Category: Experimental                                     November 2020
ISSN: 2070-1721


                         JSON Type Definition

Abstract

  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.

Status of This Memo

  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 Notice

  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.

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 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:

  *  Provide an unambiguous description of the overall structure of a
     JSON document.

  *  Be able to describe common JSON data types and structures (that
     is, the data types and structures necessary to support most JSON
     documents and that are widely understood in an interoperable way
     by JSON implementations).

  *  Provide a single format that is 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
     accept as input or produce as output.

  *  Enable code generation from JTD schemas.  JTD schemas are meant to
     be easy to convert into data structures idiomatic to mainstream
     programming languages.

  *  Provide a standardized format for error indicators when data does
     not conform with a schema.

  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:

  *  Signed and unsigned 8-, 16-, and 32-bit integers.  A tool that
     converts JTD schemas into code can use "int8_t", "uint8_t",
     "int16_t", etc., or their 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, corresponding to some sort of
     struct or record.  The "properties" form of JSON objects is akin
     to a C++ "struct".

  *  A "values" form of JSON objects, corresponding to some sort of
     dictionary or associative array.  The "values" form of JSON
     objects is akin to a C++ "std::map".

  *  A "discriminator" form of JSON objects, corresponding to a
     discriminated (or "tagged") union.  The "discriminator" form of
     JSON objects is akin to a C++ "std::variant".

  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.

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

1.2.  Scope of Experiment

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

2.  Syntax

  This section describes when a JSON document is a correct JTD schema.
  Because Concise Data Definition Language (CDDL) is well suited to the
  task of defining complex JSON formats, such as JTD schemas, this
  section uses CDDL to describe the format of JTD schemas.

  JTD schemas may recursively contain other schemas.  In this document,
  a "root schema" is one that is not contained within another schema,
  i.e., it is "top level".

  A JTD schema is a JSON object taking on an appropriate form.  JTD
  schemas may contain "additional data", discussed in Section 2.3.
  Root JTD schemas may optionally contain definitions (a mapping from
  names to schemas).

  A correct root JTD schema MUST match the "root-schema" CDDL rule
  described in this section.  A correct non-root JTD schema MUST match
  the "schema" CDDL rule described in this section.

  ; root-schema is identical to schema, but additionally allows for
  ; definitions.
  ;
  ; definitions are prohibited from appearing on non-root schemas.
  root-schema = {
    ? definitions: { * tstr => { schema}},
    schema,
  }
  ; schema is the main CDDL rule defining a JTD schema.
  ;
  ; All JTD schemas are JSON objects taking on one of eight forms
  ; listed here.
  schema = (
    ref //
    type //
    enum //
    elements //
    properties //
    values //
    discriminator //
    empty //
  )
  ; shared is a CDDL rule containing properties that all eight schema
  ; forms share.
  shared = (
    ? metadata: { * tstr => any },
    ? nullable: bool,
  )
  ; empty describes the "empty" schema form.
  empty = shared
  ; ref describes the "ref" schema form.
  ;
  ; There are additional constraints on this form that cannot be
  ; expressed in CDDL. Section 2.2.2 describes these additional
  ; constraints in detail.
  ref = ( ref: tstr, shared )
  ; type describes the "type" schema form.
  type = (
    type: "boolean"
      / "float32"
      / "float64"
      / "int8"
      / "uint8"
      / "int16"
      / "uint16"
      / "int32"
      / "uint32"
      / "string"
      / "timestamp",
    shared,
  )
  ; enum describes the "enum" schema form.
  ;
  ; There are additional constraints on this form that cannot be
  ; expressed in CDDL. Section 2.2.4 describes these additional
  ; constraints in detail.
  enum = ( enum: [+ tstr], shared )
  ; elements describes the "elements" schema form.
  elements = ( elements: { schema }, shared )
  ; properties describes the "properties" schema form.
  ;
  ; This CDDL rule is defined so that a schema of the "properties" form
  ; may omit a member named "properties" or a member named
  ; "optionalProperties", but not both.
  ;
  ; There are additional constraints on this form that cannot be
  ; expressed in CDDL. Section 2.2.6 describes these additional
  ; constraints in detail.
  properties = (with-properties // with-optional-properties)
  with-properties = (
    properties: { * tstr => { schema }},
    ? optionalProperties: { * tstr => { schema }},
    ? additionalProperties: bool,
    shared,
  )
  with-optional-properties = (
    ? properties: { * tstr => { schema }},
    optionalProperties: { * tstr => { schema }},
    ? additionalProperties: bool,
    shared,
  )
  ; values describes the "values" schema form.
  values = ( values: { schema }, shared )
  ; discriminator describes the "discriminator" schema form.
  ;
  ; There are additional constraints on this form that cannot be
  ; expressed in CDDL. Section 2.2.8 describes these additional
  ; constraints in detail.
  discriminator = (
    discriminator: tstr,
    ; Note well: this rule is defined in terms of the "properties"
    ; CDDL rule, not the "schema" CDDL rule.
    mapping: { * tstr => { properties } }
    shared,
  )

                  Figure 1: CDDL Definition of a Schema

  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.

2.1.  Root vs. Non-root 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".

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 defined
  so as to be mutually exclusive; a schema cannot satisfy multiple
  forms at once.

2.2.1.  Empty

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

2.2.2.  Ref

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

  For a schema of the "ref" form to be correct, the value of the member
  named "ref" must refer to one of the definitions found at the root
  level of the schema it appears in.  More formally, for a schema _S_
  of the "ref" form:

  *  Let _B_ be the root schema containing the schema or the schema
     itself if it is a root schema.

  *  Let _R_ be the value of the member of _S_ with the name "ref".

  If the schema is correct, then _B_ MUST have a member _D_ with the
  name "definitions", and _D_ MUST contain a member whose name equals
  _R_.

  Thus,

     {
       "definitions": {
         "coordinates": {
           "properties": {
             "lat": { "type": "float32" },
             "lng": { "type": "float32" }
           }
         }
       },
       "properties": {
         "user_location": { "ref": "coordinates" },
         "server_location": { "ref": "coordinates" }
       }
     }

  is a correct JTD schema and demonstrates the point of the "ref" form:
  to avoid redefining the same thing twice.  However,

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

2.2.3.  Type

  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.

2.2.4.  Enum

  The "enum" form is defined by the "enum" CDDL rule in Figure 1.  The
  semantics of the "enum" form are described in Section 3.3.4.

  For a schema of the "enum" form to be correct, the value of the
  member named "enum" must be a nonempty array of strings, and that
  array must not contain duplicate values.  More formally, for a schema
  _S_ of the "enum" form:

  *  Let _E_ be the value of the member of _S_ with name "enum".

  If the schema is correct, then there MUST NOT exist any pair of
  elements of _E_ that encode equal string values, where string
  equality is defined as in Section 8.3 of [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.  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.

2.2.6.  Properties

  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.

2.2.7.  Values

  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.

2.2.8.  Discriminator

  The "discriminator" form is defined by the "discriminator" CDDL rule
  in Figure 1.  The semantics of the "discriminator" form are described
  in Section 3.3.8.  Understanding the semantics of the "discriminator"
  form will likely aid the reader in understanding why this section
  provides constraints on the "discriminator" form beyond those in
  Figure 1.

  To prevent ambiguous or unsatisfiable constraints on the
  "discriminator" property of a tagged union, an additional constraint
  on schemas of the "discriminator" form exists.  For schemas of the
  "discriminator" form:

  *  Let _D_ be the member of the schema with the name "discriminator".

  *  Let _M_ be the member of the schema with the name "mapping".

  If the schema is correct, then all member values _S_ of _M_ will be
  schemas of the "properties" form.  For each _S_:

  *  If _S_ has a member _N_ whose name equals "nullable", _N_'s value
     MUST NOT be the JSON primitive value "true".

  *  For each member _P_ of _S_ whose name equals "properties" or
     "optionalProperties", _P_'s value, which must be an object, MUST
     NOT contain any members whose name equals _D_'s value.

  Thus,

     {
       "discriminator": "event_type",
       "mapping": {
         "can_the_object_be_null_or_not?": {
           "nullable": true,
           "properties": { "foo": { "type": "string" } }}
         }
       }
     }

  is an incorrect schema, as a member of "mapping" has a member named
  "nullable" whose value is "true".  This would suggest that the
  instance may be null.  Yet, the top-level schema lacks such a
  "nullable" set to "true", which would suggest that the instance in
  fact cannot be null.  If this were a correct JTD schema, it would be
  unclear which piece of information takes precedence.

  JTD handles such possible ambiguity by disallowing, at the syntactic
  level, the possibility of contradictory specifications of whether an
  instance described by a schema of the "discriminator" form may be
  null.  The schemas in a discriminator "mapping" cannot have
  "nullable" set to "true"; only the discriminator itself can use
  "nullable" in this way.

  It also follows that

     {
       "discriminator": "event_type",
       "mapping": {
         "is_event_type_a_string_or_a_float32?": {
           "properties": { "event_type": { "type": "float32" }}
         }
       }
     }

  and

     {
       "discriminator": "event_type",
       "mapping": {
         "is_event_type_a_string_or_an_optional_float32?": {
           "optionalProperties": { "event_type": { "type": "float32" }}
         }
       }
     }

  are incorrect schemas, as "event_type" is both the value of
  "discriminator" and a member name in one of the "mapping" member
  "properties" or "optionalProperties".  This is ambiguous, because
  ordinarily the "discriminator" keyword would indicate that
  "event_type" is expected to be a string, but another part of the
  schema specifies that "event_type" is expected to be a number.

  JTD handles such possible ambiguity by disallowing, at the syntactic
  level, the possibility of contradictory specifications of
  discriminator "tags".  Discriminator "tags" cannot be redefined in
  other parts of the schema.

  By contrast,

     {
       "discriminator": "event_type",
       "mapping": {
         "account_deleted": {
           "properties": {
             "account_id": { "type": "string" }
           }
         },
         "account_payment_plan_changed": {
           "properties": {
             "account_id": { "type": "string" },
             "payment_plan": { "enum": ["FREE", "PAID"] }
           },
           "optionalProperties": {
             "upgraded_by": { "type": "string" }
           }
         }
       }
     }

  is a correct schema, describing a pattern of data common in JSON-
  based messaging systems.  Section 3.3.8 provides examples of what
  this schema accepts and rejects.

2.3.  Extending JTD's Syntax

  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.

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.

3.1.  Allowing Additional Properties

  Users will have different desired behavior with respect to
  "unspecified" members in an instance.  For example, consider 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, 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".

3.2.  Errors

  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:

  *  A member with the name "instancePath", whose value is a JSON
     string encoding a JSON Pointer.  This JSON Pointer will point 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 will point 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 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.

3.3.1.  Empty

  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.

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 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:

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

3.3.3.  Type

  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:

        Let _T_ be the value of the member with the name "type".  The
        following table describes whether the instance is accepted, as
        a function of _T_'s value:

        +============+=========================================+
           | 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 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             |
   +--------+---------------------------+---------------------------+

                   Table 2: Ranges for Integer Types

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

3.3.4.  Enum

  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:

        Let _E_ be the value of the schema member with the name "enum".
        The instance is accepted if and only if it is equal to one of
        the elements of _E_.

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

3.3.5.  Elements

  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:

        o  The instance is an array.  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 "elements".

        o  If the instance is an array, then every element of the
           instance must be accepted by _S_. Otherwise, the error
           indicators for this case are the union of all the errors
           arising from evaluating _S_ against elements of the
           instance.

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

3.3.6.  Properties

  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:

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

        o  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 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:

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

        o  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

  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 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 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:

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

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

4.  IANA Considerations

  This document has no IANA actions.

5.  Security Considerations

  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.

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.

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

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

A.2.  Support for Non-root Definitions

  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.

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

Appendix B.  Comparison with CDDL

  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 }

Appendix C.  Example

  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]:

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

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

Acknowledgments

  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.

Author's Address

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

  Email: [email protected]