RFC 8927 JSON 타입 정의 2020년 11월
Carion 실험적 [Page]
스트림:
독립 제출
RFC:
8927
분류:
실험적
게시:
ISSN:
2070-1721
저자:
U. Carion
Segment

RFC 8927

JSON 타입 정의

초록

이 문서는 JavaScript Object Notation(JSON) 메시지의 형태를 설명하기 위한 JSON Type Definition(JTD)이라는 형식을 제안한다. 주요 목표는 스키마로부터 코드 생성을 가능하게 하고, 표준화된 오류 표시자를 사용한 이식 가능한 검증을 가능하게 하는 것이다. 이를 위해 JTD는 주류 프로그래밍 언어의 타입 시스템보다 더 표현력이 높지 않도록 의도적으로 제한되어 있다. 이러한 의도적인 제한과 JTD 스키마를 JSON 문서로 만들기로 한 결정은 JTD 위의 도구를 더 쉽게 구축할 수 있게 한다.

이 문서는 IETF 합의를 갖고 있지 않으며, JTD 개념에 대한 실험을 촉진하기 위해 여기에 제시된다.

이 메모의 상태

이 문서는 인터넷 표준 트랙 명세가 아니며, 검토, 실험적 구현 및 평가를 위해 게시된다.

이 문서는 인터넷 커뮤니티를 위한 실험적 프로토콜을 정의한다. 이는 다른 어떤 RFC 스트림과도 독립적으로 RFC 시리즈에 기여한 문서이다. RFC Editor는 재량에 따라 이 문서를 게시하기로 선택했으며, 구현 또는 배포에 대한 가치에 대해 어떠한 진술도 하지 않는다. RFC Editor가 게시를 승인한 문서는 어떤 수준의 인터넷 표준 후보도 아니다. RFC 7841의 Section 2를 참조하라.

이 문서의 현재 상태, 정오표, 피드백 제공 방법에 대한 정보는 https://www.rfc-editor.org/info/rfc8927에서 얻을 수 있다.

목차

1. 소개

이 문서는 JSON [RFC8259]을 위한 JSON Type Definition (JTD)이라는 스키마 언어를 설명한다.

JSON 데이터를 설명하기 위한 선택지는 많이 있다. JTD의 틈새는 스키마로부터 코드 생성을 가능하게 하는 데 초점을 맞추는 것이다. 이를 위해 JTD의 표현력은 주류 프로그래밍 언어의 타입 시스템에서 표현할 수 있는 것보다 더 강력하지 않도록 의도적으로 제한된다.

JTD의 목표는 다음과 같다.

JTD는 다소 최소한의 스키마 언어로 의도적으로 설계되었다. 따라서 JTD가 JSON의 일부 범주를 설명할 수는 있지만, 자기 자신의 구조는 설명할 수 없다. 이 문서는 JTD의 구문을 설명하기 위해 Concise Data Definition Language(CDDL) [RFC8610]를 사용한다. 스키마 언어의 표현력을 최소한으로 유지함으로써, JTD는 코드 생성과 표준화된 오류 표시자를 더 쉽게 구현할 수 있게 한다.

이 문서의 예제는 C++ 프로그래밍 언어의 구성을 사용한다. 이러한 예제는 독자가 JTD의 원리를 이해하는 데 도움을 주기 위해 제공되며, 어떤 방식으로도 제한적이지 않다.

JTD의 기능 집합은 JSON을 사용하는 애플리케이션에서 흔한 패턴을 표현하면서도, 널리 사용되는 프로그래밍 언어와 명확한 대응 관계를 갖도록 설계되었다. 따라서 JTD는 다음을 지원한다.

JSON에서 흔한 패턴이라는 원칙 때문에 JTD는 64비트 정수를 지원하지 않는다. 이러한 정수는 보통 JSON을 통해 상호운용되지 않는 방식(즉, Section 2.2 of [RFC7493]의 권장사항을 무시하는 방식)이나 서로 일관되지 않은 방식으로 전송되기 때문이다. 부록 A.1은 JTD가 64비트 정수를 지원하지 않는 이유를 더 자세히 설명한다.

일반적인 프로그래밍 언어와의 명확한 대응 관계라는 원칙 때문에 JTD는 예를 들어 2**53-1까지의 정수를 위한 데이터 타입을 지원하지 않는다.

많은 사용 사례에서 JTD의 표현력을 가진 스키마 언어로 충분할 것으로 예상된다. 더 표현력이 높은 언어가 필요한 경우에는 CDDL 및 기타 대안이 존재한다.

이 문서는 IETF 합의를 갖고 있지 않으며, JTD 개념에 대한 실험을 촉진하기 위해 여기에 제시된다. 이 실험의 목적은 JTD에 대한 경험을 얻고, 그에 따라 이 작업을 수정할 가능성을 살펴보는 것이다. JTD가 가치 있고 널리 쓰이는 접근 방식이라고 판단되면, 추가 논의와 개정을 위해 IETF에 제출될 수 있다.

이 문서는 다음과 같은 구조를 가진다. Section 2는 JTD의 구문을 정의한다. Section 3은 JTD의 의미론을 설명한다. 여기에는 어떤 데이터가 스키마를 만족하는지 판정하는 것과 데이터가 만족스럽지 않을 때 어떤 오류 표시자를 생성해야 하는지가 포함된다. 부록 A는 특정 기능이 JTD에서 생략된 이유를 논의한다. 부록 B는 다양한 JTD 스키마와 그 CDDL 대응물을 제시한다.

1.1. 용어

이 문서의 핵심 단어 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL"은 여기에 표시된 것처럼 모두 대문자로 나타날 때, 그리고 오직 그럴 때에만 BCP 14 [RFC2119] [RFC8174]에 설명된 대로 해석되어야 한다.

이 문서에 나타나는 "JSON Pointer"라는 용어는 [RFC6901]에 정의된 대로 이해되어야 한다.

이 문서의 "object", "member", "array", "number", "name", "string"이라는 용어는 [RFC8259]에 설명된 대로 해석되어야 한다.

이 문서에 나타나는 "instance"라는 용어는 JTD 스키마에 대해 검증되는 JSON 값을 가리킨다. 이 값은 전체 JSON 문서일 수도 있고, JSON 문서 안에 포함된 값일 수도 있다.

1.2. 실험의 범위

JTD는 하나의 실험이다. 이 실험에 참여한다는 것은 JTD를 사용하여 교환되는 JSON 메시지를 검증하거나 문서화하는 것, 또는 JTD 위에 도구를 구축하는 것으로 이루어진다. 이 실험 결과에 대한 피드백은 저자에게 이메일로 보낼 수 있다. 이 실험의 참여자는 주로 JSON 기반 API를 제공하거나 소비하는 노드일 것으로 예상된다.

노드는 JTD 스키마에 대해 JSON 메시지를 검증하고 있거나, 다른 노드가 그렇게 하도록 의존하고 있다면 자신이 이 실험에 참여하고 있음을 안다. JTD 스키마로부터 생성된 코드를 실행하는 경우에도 노드는 이 실험에 참여하고 있다.

이 실험이 "빠져나가는" 위험은, JTD를 지원하는 노드가 그러한 지원을 갖지 않은 다른 노드가 어떤 JTD 스키마에 대해 메시지를 검증할 것으로 기대하는 형태를 띤다. 그러한 경우 결과는 노드들이 정보를 올바르게 교환하지 못하는 것이 될 가능성이 높다.

이 실험은 JTD가 여러 독립 당사자에 의해 구현되고, 이들 당사자가 내부 시스템 안에서 또는 독립 당사자가 운영하는 시스템 사이에서 정보 교환을 촉진하기 위해 JTD를 성공적으로 사용할 때 성공한 것으로 간주된다.

이 실험이 성공한 것으로 간주되고 JTD가 가치 있고 널리 쓰이는 접근 방식이라고 판단되면, 추가 논의와 개정을 위해 IETF에 제출될 수 있다. 이 논의와 개정의 가능한 결과 중 하나는 작업 그룹이 JTD의 표준 트랙 명세를 생성하는 것일 수 있다.

JTD의 일부 구현과 JTD와 관련된 코드 생성기 및 기타 도구는 <https://github.com/jsontypedef>에서 사용할 수 있다.

2. 구문

이 절은 JSON 문서가 언제 올바른 JTD 스키마인지 설명한다. Concise Data Definition Language(CDDL)는 JTD 스키마와 같은 복잡한 JSON 형식을 정의하는 작업에 적합하므로, 이 절은 JTD 스키마의 형식을 설명하기 위해 CDDL을 사용한다.

JTD 스키마는 재귀적으로 다른 스키마를 포함할 수 있다. 이 문서에서 "root schema"란 다른 스키마 안에 포함되지 않은 스키마, 즉 "top level"인 스키마를 말한다.

JTD 스키마는 적절한 형식을 취하는 JSON 객체이다. JTD 스키마는 Section 2.3에서 논의하는 "additional data"를 포함할 수 있다. 루트 JTD 스키마는 선택적으로 정의(이름에서 스키마로의 매핑)를 포함할 수 있다.

올바른 루트 JTD 스키마는 이 절에서 설명하는 "root-schema" CDDL 규칙과 일치해야 MUST 한다. 올바른 비루트 JTD 스키마는 이 절에서 설명하는 "schema" CDDL 규칙과 일치해야 MUST 한다.

; 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,
)
그림 1: 스키마의 CDDL 정의

이 절의 나머지 부분은 CDDL로 표현할 수 없는 JTD 스키마의 제약을 설명한다. 또한 유효한 JTD 스키마와 유효하지 않은 JTD 스키마의 예제를 제공한다.

2.1. 루트 스키마와 비루트 스키마

그림 1의 "root-schema" 규칙은 "definitions"라는 이름의 멤버를 허용하지만, "schema" 규칙은 그러한 멤버를 허용하지 않는다. 이는 루트(즉, "top-level") JTD 스키마만 "definitions" 객체를 가질 수 있으며, 하위 스키마는 그럴 수 없음을 의미한다.

따라서,

   { "definitions": {} }

는 올바른 JTD 스키마이지만,

   {
     "definitions": {
       "foo": {
         "definitions": {}
       }
     }
   }

는 그렇지 않다. 하위 스키마(예: "/definitions/foo"의 객체)는 "definitions"라는 이름의 멤버를 가져서는 안 되기 때문이다.

2.2. 형식

JTD 스키마(즉, 그림 1의 "schema" CDDL 규칙을 만족하는 JSON 객체)는 여덟 가지 형식 중 하나를 취해야 한다. 이 형식들은 상호 배타적이도록 정의되어 있으며, 하나의 스키마가 동시에 여러 형식을 만족할 수 없다.

2.2.1. 빈 형식

"empty" 형식은 그림 1의 "empty" CDDL 규칙으로 정의된다. "empty" 형식의 의미론은 Section 3.3.1에 설명되어 있다.

"empty"라는 이름에도 불구하고, "empty" 형식의 스키마가 반드시 빈 JSON 객체인 것은 아니다. 여덟 형식 중 어떤 형식의 스키마와 마찬가지로, "empty" 형식의 스키마도 "nullable"(그 값은 "true" 또는 "false"여야 함)이라는 이름의 멤버나 "metadata"(그 값은 객체여야 함)라는 이름의 멤버, 또는 둘 다를 포함할 수 있다.

따라서,

   {}

   { "nullable": true }

   { "nullable": true, "metadata": { "foo": "bar" }}

는 "empty" 형식의 올바른 JTD 스키마이지만,

   { "nullable": "foo" }

는 그렇지 않다. "nullable"이라는 이름의 멤버 값은 "true" 또는 "false"여야 하기 때문이다.

2.2.2. 참조

"ref" 형식은 그림 1의 "ref" CDDL 규칙으로 정의된다. "ref" 형식의 의미론은 Section 3.3.2에 설명되어 있다.

"ref" 형식의 스키마가 올바르려면, "ref"라는 이름의 멤버 값은 그 스키마가 나타나는 스키마의 루트 수준에서 발견되는 정의 중 하나를 참조해야 한다. 더 형식적으로, "ref" 형식의 스키마 S에 대해:

  • B를 해당 스키마를 포함하는 루트 스키마, 또는 그 스키마 자체가 루트 스키마인 경우에는 그 스키마 자체라고 하자.
  • R을 이름이 "ref"인 S의 멤버 값이라고 하자.

스키마가 올바르다면, B는 이름이 "definitions"인 멤버 D를 가져야 MUST 하며, D는 그 이름이 R과 같은 멤버를 포함해야 MUST 한다.

따라서,

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

는 올바른 JTD 스키마이며, 같은 것을 두 번 다시 정의하지 않기 위한 "ref" 형식의 목적을 보여준다. 그러나,

   { "ref": "foo" }

는 올바른 JTD 스키마가 아니다. 최상위 "definitions"가 없으므로 "ref" 형식이 올바를 수 없기 때문이다. 마찬가지로,

   { "definitions": { "foo": {}}, "ref": "bar" }

는 올바른 JTD 스키마가 아니다. 최상위 "definitions" 안에 "bar"라는 이름의 멤버가 없기 때문이다.

2.2.3. 타입

"type" 형식은 그림 1의 "type" CDDL 규칙으로 정의된다. "type" 형식의 의미론은 Section 3.3.3에 설명되어 있다.

"type" 형식의 올바른 JTD 스키마 예로,

   { "type": "uint8" }

는 올바른 JTD 스키마이지만,

   { "type": true }

   { "type": "foo" }

는 올바른 스키마가 아니다. "true"와 JSON 문자열 "foo" 모두 그림 1의 "type" CDDL 규칙에 설명된 "type" 멤버의 허용 값 목록에 없기 때문이다.

2.2.4. 열거형

"enum" 형식은 그림 1의 "enum" CDDL 규칙으로 정의된다. "enum" 형식의 의미론은 Section 3.3.4에 설명되어 있다.

"enum" 형식의 스키마가 올바르려면, "enum"이라는 이름의 멤버 값은 비어 있지 않은 문자열 배열이어야 하며, 그 배열은 중복 값을 포함해서는 안 된다. 더 형식적으로, "enum" 형식의 스키마 S에 대해:

  • E를 이름이 "enum"인 S의 멤버 값이라고 하자.

스키마가 올바르다면, E의 요소 쌍 중 같은 문자열 값을 인코딩하는 어떤 쌍도 존재해서는 MUST NOT 안 된다. 여기서 문자열 동등성은 Section 8.3 of [RFC8259]에 정의된 대로이다.

따라서,

   { "enum": [] }

는 올바른 JTD 스키마가 아니다. "enum"이라는 이름의 멤버 값은 비어 있지 않아야 하기 때문이다. 또한

   { "enum": ["a\\b", "a\u005Cb"] }

는 올바른 JTD 스키마가 아닌데, 그 이유는

   "a\\b"

   "a\u005Cb"

Section 8.3 of [RFC8259]에 제시된 문자열 동등성 정의에 따라 같은 문자열을 인코딩하기 때문이다. 반대로,

   { "enum": ["PENDING", "IN_PROGRESS", "DONE" ]}

는 "enum" 형식의 올바른 JTD 스키마의 예이다.

2.2.5. 요소

"elements" 형식은 그림 1의 "elements" CDDL 규칙으로 정의된다. "elements" 형식의 의미론은 Section 3.3.5에 설명되어 있다.

"elements" 형식의 올바른 JTD 스키마 예로,

   { "elements": { "type": "uint8" }}

는 올바른 JTD 스키마이지만,

   { "elements": true }

   { "elements": { "type": "foo" } }

는 올바른 스키마가 아니다. 왜냐하면

   true

   { "type": "foo" }

도 올바른 JTD 스키마가 아니며, "elements"라는 이름의 멤버 값은 올바른 JTD 스키마여야 하기 때문이다.

2.2.6. 속성

"properties" 형식은 그림 1의 "properties" CDDL 규칙으로 정의된다. "properties" 형식의 의미론은 Section 3.3.6에 설명되어 있다.

"properties" 형식의 스키마가 올바르려면, 속성은 필수(즉, "properties" 안에 있음)이거나 선택적(즉, "optionalProperties" 안에 있음)이어야 하며, 둘 다일 수는 없다.

더 형식적으로, 어떤 스키마가 "properties"라는 이름의 멤버(값은 P)와 "optionalProperties"라는 이름의 다른 멤버(값은 O)를 모두 가진다면, OP는 공통된 멤버 이름을 가져서는 MUST NOT 안 된다. 즉, P의 어떤 멤버도 Section 8.3 of [RFC8259]에 제시된 문자열 동등성 정의에 따라 O의 어떤 멤버 이름과도 같은 이름을 가져서는 안 된다.

따라서,

   {
     "properties": { "confusing": {} },
     "optionalProperties": { "confusing": {} }
   }

는 올바른 JTD 스키마가 아니다. "confusing"이 "properties"와 "optionalProperties" 모두에 나타나기 때문이다. 반대로,

   {
     "properties": {
       "users": {
         "elements": {
           "properties": {
             "id": { "type": "string" },
             "name": { "type": "string" },
             "create_time": { "type": "timestamp" }
           },
           "optionalProperties": {
             "delete_time": { "type": "timestamp" }
           }
         }
       },
       "next_page_token": { "type": "string" }
     }
   }

는 "properties" 형식의 올바른 JTD 스키마이며, 사용자들의 페이지가 매겨진 목록을 설명하고 JTD 스키마 구문의 재귀적 성격을 보여준다.

2.2.7.

"values" 형식은 그림 1의 "values" CDDL 규칙으로 정의된다. "values" 형식의 의미론은 Section 3.3.7에 설명되어 있다.

"values" 형식의 올바른 JTD 스키마 예로,

   { "values": { "type": "uint8" }}

는 올바른 JTD 스키마이지만,

   { "values": true }

   { "values": { "type": "foo" } }

는 올바른 스키마가 아니다. 왜냐하면

   true

   { "type": "foo" }

도 올바른 JTD 스키마가 아니며, "values"라는 이름의 멤버 값은 올바른 JTD 스키마여야 하기 때문이다.

2.2.8. 판별자

"discriminator" 형식은 그림 1의 "discriminator" CDDL 규칙으로 정의된다. "discriminator" 형식의 의미론은 Section 3.3.8에 설명되어 있다. "discriminator" 형식의 의미론을 이해하면, 이 절이 그림 1에 있는 것 이상의 "discriminator" 형식 제약을 제공하는 이유를 이해하는 데 도움이 될 것이다.

tagged union의 "discriminator" 속성에 대해 모호하거나 만족할 수 없는 제약을 방지하기 위해, "discriminator" 형식의 스키마에는 추가 제약이 존재한다. "discriminator" 형식의 스키마에 대해:

  • D를 이름이 "discriminator"인 스키마의 멤버라고 하자.
  • M을 이름이 "mapping"인 스키마의 멤버라고 하자.

스키마가 올바르다면, M의 모든 멤버 값 S는 "properties" 형식의 스키마가 된다. 각 S에 대해:

  • S가 이름이 "nullable"과 같은 멤버 N을 가지고 있다면, N의 값은 JSON 원시 값 "true"여서는 MUST NOT 안 된다.
  • S의 각 멤버 P 중 이름이 "properties" 또는 "optionalProperties"와 같은 멤버에 대해, 객체여야 하는 P의 값은 D의 값과 이름이 같은 멤버를 포함해서는 MUST NOT 안 된다.

따라서,

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

는 잘못된 스키마이다. "mapping"의 한 멤버가 "nullable"이라는 이름의 멤버를 가지고 있고 그 값이 "true"이기 때문이다. 이는 인스턴스가 null일 수 있음을 시사한다. 그러나 최상위 스키마에는 "true"로 설정된 그러한 "nullable"이 없어서, 실제로 인스턴스가 null일 수 없음을 시사한다. 이것이 올바른 JTD 스키마라면, 어떤 정보가 우선하는지 불명확할 것이다.

JTD는 "discriminator" 형식의 스키마가 설명하는 인스턴스가 null일 수 있는지에 대한 모순된 명세의 가능성을 구문 수준에서 허용하지 않음으로써 이러한 가능한 모호성을 처리한다. discriminator "mapping" 안의 스키마는 "nullable"을 "true"로 설정할 수 없으며, discriminator 자체만 이런 방식으로 "nullable"을 사용할 수 있다.

또한 다음도 이에 따른다.

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

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

는 잘못된 스키마이다. "event_type"이 "discriminator"의 값이면서 동시에 "mapping" 멤버의 "properties" 또는 "optionalProperties" 중 하나의 멤버 이름이기 때문이다. 이는 모호하다. 일반적으로 "discriminator" 키워드는 "event_type"이 문자열일 것으로 기대됨을 나타내지만, 스키마의 다른 부분은 "event_type"이 숫자일 것으로 기대된다고 명시하기 때문이다.

JTD는 discriminator "tags"의 모순된 명세 가능성을 구문 수준에서 허용하지 않음으로써 이러한 가능한 모호성을 처리한다. Discriminator "tags"는 스키마의 다른 부분에서 다시 정의될 수 없다.

반대로,

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

는 올바른 스키마이며, JSON 기반 메시징 시스템에서 흔한 데이터 패턴을 설명한다. Section 3.3.8은 이 스키마가 무엇을 수락하고 거부하는지에 대한 예제를 제공한다.

2.3. JTD 구문 확장

이 문서는 Section 3에 설명된 JTD 스키마 검증을 위한 어떤 확장 메커니즘도 설명하지 않는다. 그러나 스키마는 선택적으로 "metadata" 키워드를 포함하도록 정의되며, 그 값은 임의의 JSON 객체이다. 이 객체의 멤버를 "metadata members"라고 부른다.

사용자는 검증과 관련 없는 정보를 전달하기 위해 JTD 스키마에 metadata members를 추가할 MAY 수 있다. 예를 들어, 이러한 metadata members는 코드 생성기에 힌트를 제공하거나, 스키마로부터 사용자 인터페이스를 생성하는 라이브러리의 어떤 특수한 동작을 트리거할 수 있다.

사용자는 metadata members가 다른 당사자에게 이해될 것이라고 기대해서는 SHOULD NOT 안 된다. 그 결과, 다른 당사자와의 일관된 검증이 요구사항이라면, 사용자는 Section 3에 설명된 스키마 검증이 작동하는 방식에 영향을 주기 위해 metadata members를 사용해서는 MUST NOT 안 된다.

사용자는 이러한 metadata members를 다른 당사자가 지원한다고 어떤 방식으로든 알려져 있다면, metadata members가 다른 당사자에게 이해될 것이라고 기대할 MAY 수 있고, metadata members를 사용하여 스키마 검증이 작동하는 방식에 영향을 줄 MAY 수 있다. 예를 들어, 두 당사자는 검증에 영향을 주는 사용자 지정 metadata member를 가진 확장 JTD를 지원하기로 별도의 경로를 통해 합의할 수 있다.

3. 의미론

이 절은 인스턴스가 올바른 JTD 스키마에 대해 언제 유효한지와 인스턴스가 유효하지 않을 때 생성할 오류 표시자를 설명한다.

3.1. 추가 속성 허용

사용자는 인스턴스의 "unspecified" 멤버에 대해 서로 다른 원하는 동작을 가질 수 있다. 예를 들어, 그림 2의 JTD 스키마를 고려하라.

{ "properties": { "a": { "type": "string" }}}
그림 2: 예시 JTD 스키마

일부 사용자는 다음이

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

그림 2의 스키마를 만족한다고 기대할 수 있다. 다른 사용자는 "b"가 스키마에서 설명된 속성 중 하나가 아니므로 이에 동의하지 않을 수 있다. 이 문서에서 이 예의 "b"와 같은 "unspecified" 멤버를 허용하는 것은 평가가 "allow additional properties" 모드에 있을 때 발생한다.

스키마 평가는 기본적으로 추가 속성을 허용하지 않지만, 스키마가 "additionalProperties"라는 이름의 멤버를 포함하고, 그 멤버의 값이 "true"인 경우 이를 재정의할 수 있다.

더 형식적으로, 스키마 S의 평가가 "allow additional properties" 모드에 있는 것은 S에 이름이 "additionalProperties"와 같고 값이 boolean "true"인 멤버가 존재하는 경우이다. 그렇지 않으면 S의 평가는 "allow additional properties" 모드에 있지 않다.

알 수 없는 속성을 허용하는 것이 스키마 평가에 어떤 영향을 주는지는 Section 3.3.6을 참조하라. 간단히 말해, 스키마

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

는 다음을 거부한다.

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

그러나 스키마

   {
     "additionalProperties": true,
     "properties": { "a": { "type": "string" }}
   }

는 다음을 수락한다.

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

"additionalProperties"는 하위 스키마에 "상속"되지 않는다는 점에 유의하라. 예를 들어, JTD 스키마

   {
     "additionalProperties": true,
     "properties": {
       "a": {
         "properties": {
           "b": { "type": "string" }
         }
       }
     }
   }

는 다음을 수락하지만

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

다음은 거부한다.

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

이는 루트 수준의 "additionalProperties"가 하위 스키마의 동작에 영향을 주지 않기 때문이다.

그림 1에서 볼 수 있듯이, "properties" 형식의 스키마만 "additionalProperties"라는 이름의 멤버를 가질 수 있다는 점에 유의하라.

3.2. 오류

일관된 검증 오류 처리를 촉진하기 위해, 이 문서는 표준 오류 표시자 형식을 명시한다. 구현은 이 표준 형식으로 오류 표시자를 생성하는 것을 지원해야 SHOULD 한다.

표준 오류 표시자 형식은 JSON 배열이다. 이 배열의 요소 순서는 명시되지 않는다. 이 배열의 요소는 다음을 가진 JSON 객체이다.

  • 이름이 "instancePath"인 멤버. 그 값은 JSON Pointer를 인코딩하는 JSON 문자열이다. 이 JSON Pointer는 거부된 인스턴스의 부분을 가리킨다.
  • 이름이 "schemaPath"인 멤버. 그 값은 JSON Pointer를 인코딩하는 JSON 문자열이다. 이 JSON Pointer는 인스턴스를 거부한 스키마의 부분을 가리킨다.

"instancePath"와 "schemaPath"의 값은 스키마의 형식에 따라 달라지며, Section 3.3에 자세히 설명되어 있다.

3.3. 형식

이 절은 여덟 가지 JTD 스키마 형식 각각에 대해, 인스턴스가 수락되는지 여부를 지시하는 규칙과 인스턴스가 유효하지 않을 때 생성할 오류 표시자를 설명한다.

올바른 스키마가 취할 수 있는 형식은 Section 2에 형식적으로 설명되어 있다.

3.3.1. 빈 형식

"empty" 형식은 값이 알 수 없거나 예측할 수 없거나 그 밖의 방식으로 스키마에 의해 제약되지 않는 인스턴스를 설명하기 위한 것이다. "empty" 형식의 구문은 Section 2.2.1에 설명되어 있다.

스키마가 "empty" 형식이면 모든 인스턴스를 수락한다. "empty" 형식의 스키마는 어떤 오류 표시자도 생성하지 않는다.

3.3.2. 참조

"ref" 형식은 스키마가 루트 스키마의 "definitions" 안에 있는 어떤 것의 관점에서 정의될 때 사용된다. "ref" 형식은 스키마를 덜 반복적으로 만들고 재귀 구조를 설명할 수도 있게 한다. "ref" 형식의 구문은 Section 2.2.2에 설명되어 있다.

스키마가 "ref" 형식이면 다음과 같다.

  • 스키마가 이름이 "nullable"이고 값이 boolean "true"인 멤버를 가지고 있으며, 인스턴스가 JSON 원시 값 "null"이면, 스키마는 그 인스턴스를 수락한다.

    그렇지 않으면:

    • R을 이름이 "ref"인 스키마 멤버의 값이라고 하자.
    • B를 해당 스키마를 포함하는 루트 스키마, 또는 그 스키마 자체가 루트 스키마인 경우 그 스키마 자체라고 하자.
    • D를 이름이 "definitions"인 B의 멤버라고 하자. Section 2에 따라 D가 존재함을 안다.
    • SR과 이름이 같은 D의 멤버 값이라고 하자. Section 2.2.2에 따라 S가 존재하고 스키마임을 안다.

스키마는 S가 인스턴스를 수락하는 경우에 그리고 그 경우에만 인스턴스를 수락한다. 그렇지 않으면 이 경우 반환할 오류 표시자는 S를 인스턴스에 대해 평가하여 나온 오류 표시자의 합집합이다.

예를 들어, 스키마

   {
     "definitions": { "a": { "type": "float32" }},
     "ref": "a"
   }

는 다음을 수락하지만

   123

다음은 거부한다.

   null

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/definitions/a/type" }]

스키마

   {
     "definitions": { "a": { "type": "float32" }},
     "ref": "a",
     "nullable": true
   }

는 다음을 수락한다.

   null

이는 스키마가 값이 "true"인 "nullable" 멤버를 가지고 있기 때문이다.

"nullable"이 "false"인 것은 이 문서에서 설명하는 어떤 형식에서도 효과가 없다는 점에 유의하라. 예를 들어, 스키마

   {
     "definitions": { "a": { "nullable": false, "type": "float32" }},
     "ref": "a",
     "nullable": true
   }

는 다음을 수락한다.

   null

다시 말해, "nullable"에 "false" 값을 넣는다고 해서 "ref" 형식의 스키마에서 "nullable" 멤버를 재정의하는 일은 결코 없다. 스키마의 "nullable" 멤버가 "false" 값을 가지는 것은 올바르지만 효과는 없다.

3.3.3. 타입

"type" 형식은 값이 boolean, number, string 또는 timestamp [RFC3339]인 인스턴스를 설명하기 위한 것이다. "type" 형식의 구문은 Section 2.2.3에 설명되어 있다.

스키마가 "type" 형식이면 다음과 같다.

  • 스키마가 이름이 "nullable"이고 값이 boolean "true"인 멤버를 가지고 있으며, 인스턴스가 JSON 원시 값 "null"이면, 스키마는 그 인스턴스를 수락한다.

    그렇지 않으면:

    • T를 이름이 "type"인 멤버의 값이라고 하자. 다음 표는 T의 값에 따라 인스턴스가 수락되는지 여부를 설명한다.
    • 표 1: Type에 대해 수락되는 값
      "T"가 ...와 같다면 인스턴스가 ...이면 수락된다
      boolean "true" 또는 "false"와 같음
      float32 JSON number
      float64 JSON number
      int8 표 2 참조
      uint8 표 2 참조
      int16 표 2 참조
      uint16 표 2 참조
      int32 표 2 참조
      uint32 표 2 참조
      string JSON string
      timestamp [RFC3339]에 설명되고, Section 3.3 of [RFC4287]에 의해 정제된 표준 형식을 따르는 JSON string

      "float32"와 "float64"는 의도에서 서로 구별된다. "float32"는 IEEE 754 단정밀도 부동소수점으로 처리되도록 의도된 데이터를 나타내는 반면, "float64"는 IEEE 754 배정밀도 부동소수점으로 처리되도록 의도된 데이터를 나타낸다. JTD 스키마로부터 코드를 생성하는 도구는 "float32"에 대해 "float64"와 다른 코드를 생성할 가능성이 높다.

T가 "int" 또는 "uint"로 시작한다면, 인스턴스가 소수부가 0인 값을 인코딩하는 JSON number인 경우에 그리고 그 경우에만 수락된다. T의 값에 따라, 이 인코딩된 숫자는 추가로 특정 범위 안에 있어야 한다.

표 2: 정수 타입의 범위
"T" 최소값(포함) 최대값(포함)
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

다음은

   10

   10.0

   1.0e1

가 소수부가 0인 값을 인코딩하는 반면,

   10.5

는 0이 아닌 소수부를 가진 숫자를 인코딩한다는 점에 유의하라. 따라서 스키마

   {"type": "int8"}

는 다음을 수락하고

   10

   10.0

   1.0e1

다음은 거부하며

   10.5

다음도 거부한다.

   false

이는 "false"가 전혀 숫자가 아니기 때문이다.

인스턴스가 수락되지 않으면, 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와 "type"이라는 이름의 스키마 멤버를 가리키는 "schemaPath"를 가져야 한다.

예를 들어, 스키마

   {"type": "boolean"}

는 다음을 수락하지만

   false

다음은 거부한다.

   127

스키마

   {"type": "float32"}

는 다음을 수락하고

   10.5

   127

다음은 거부한다.

   false

스키마

   {"type": "string"}

는 다음을 수락하고

   "1985-04-12T23:20:50.52Z"

   "foo"

다음은 거부한다.

   false

스키마

   {"type": "timestamp"}

는 다음을 수락하지만

   "1985-04-12T23:20:50.52Z"

다음은 거부하고

   "foo"

   false

스키마

   {"type": "boolean", "nullable": true}

는 다음을 수락하고

   null

   false

다음은 거부한다.

   127

이 절에서 제시된 거부된 인스턴스의 모든 예에서, 생성할 오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/type" }]

3.3.4. 열거형

"enum" 형식은 값이 주어진 문자열 값 집합 중 하나여야 하는 인스턴스를 설명하기 위한 것이다. "enum" 형식의 구문은 Section 2.2.4에 설명되어 있다.

스키마가 "enum" 형식이면 다음과 같다.

  • 스키마가 이름이 "nullable"이고 값이 boolean "true"인 멤버를 가지고 있으며, 인스턴스가 JSON 원시 값 "null"이면, 스키마는 그 인스턴스를 수락한다.

    그렇지 않으면:

    • E를 이름이 "enum"인 스키마 멤버의 값이라고 하자. 인스턴스는 E의 요소 중 하나와 같은 경우에 그리고 그 경우에만 수락된다.

인스턴스가 수락되지 않으면, 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와 이름이 "enum"인 스키마 멤버를 가리키는 "schemaPath"를 가져야 한다.

예를 들어, 스키마

   { "enum": ["PENDING", "DONE", "CANCELED"] }

는 다음을 수락하고

   "PENDING"

   "DONE"

   "CANCELED"

다음 모두를 거부한다.

   0

   1

   2

   "UNKNOWN"

   null

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/enum" }]

스키마

   { "enum": ["PENDING", "DONE", "CANCELED"], "nullable": true }

는 다음을 수락하고

   "PENDING"

   null

다음은 거부하고

   1

   "UNKNOWN"

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/enum" }]

3.3.5. 요소

"elements" 형식은 배열이어야 하는 인스턴스를 설명하기 위한 것이다. 추가 하위 스키마가 배열의 요소를 설명한다. "elements" 형식의 구문은 Section 2.2.5에 설명되어 있다.

스키마가 "elements" 형식이면 다음과 같다.

  • 스키마가 이름이 "nullable"이고 값이 boolean "true"인 멤버를 가지고 있으며, 인스턴스가 JSON 원시 값 "null"이면, 스키마는 그 인스턴스를 수락한다.

    그렇지 않으면:

    • S를 이름이 "elements"인 스키마 멤버의 값이라고 하자. 인스턴스는 다음이 모두 참인 경우에 그리고 그 경우에만 수락된다.

      • 인스턴스는 배열이다. 그렇지 않으면, 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와 이름이 "elements"인 스키마 멤버를 가리키는 "schemaPath"를 가져야 한다.
      • 인스턴스가 배열이면, 인스턴스의 모든 요소는 S에 의해 수락되어야 한다. 그렇지 않으면, 이 경우의 오류 표시자는 S를 인스턴스의 요소에 대해 평가하면서 발생한 모든 오류의 합집합이다.

예를 들어, 스키마

   {
     "elements": {
       "type": "float32"
     }
   }

는 다음을 수락하고

   []

   [1, 2, 3]

다음은 거부한다.

   null

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/elements" }]

그리고 다음을 거부한다.

   [1, 2, "foo", 3, "bar"]

오류 표시자는 다음과 같다.

   [
     { "instancePath": "/2", "schemaPath": "/elements/type" },
     { "instancePath": "/4", "schemaPath": "/elements/type" }
   ]

스키마

   {
     "elements": {
       "type": "float32"
     },
     "nullable": true
   }

는 다음을 수락하고

   null

   []

   [1, 2, 3]

다음은 거부한다.

   [1, 2, "foo", 3, "bar"]

오류 표시자는 다음과 같다.

   [
     { "instancePath": "/2", "schemaPath": "/elements/type" },
     { "instancePath": "/4", "schemaPath": "/elements/type" }
   ]

3.3.6. 속성

"properties" 형식은 "struct"로 사용되는 JSON 객체를 설명하기 위한 것이다. "properties" 형식의 구문은 Section 2.2.6에 설명되어 있다.

스키마가 "properties" 형식이면 다음과 같다.

  • 스키마가 이름이 "nullable"이고 값이 boolean "true"인 멤버를 가지고 있으며, 인스턴스가 JSON 원시 값 "null"이면, 스키마는 그 인스턴스를 수락한다.

    그렇지 않으면:

    • 인스턴스는 객체여야 한다.

      그렇지 않으면 스키마는 인스턴스를 거부한다. 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와, 그러한 스키마 멤버가 존재한다면 이름이 "properties"인 스키마 멤버를 가리키는 "schemaPath"를 가져야 한다. 그러한 멤버가 존재하지 않으면, "schemaPath"는 이름이 "optionalProperties"인 스키마 멤버를 가리켜야 한다.

    • 인스턴스가 객체이고 스키마가 "properties"라는 이름의 멤버를 가지고 있다면, P를 이름이 "properties"인 스키마 멤버의 값이라고 하자. Section 2.2.6에 따라 P가 객체임을 안다. P 안의 모든 멤버 이름에 대해, 인스턴스 안에 같은 이름의 멤버가 존재해야 한다.

      그렇지 않으면 스키마는 인스턴스를 거부한다. 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와, 방금 설명한 요구사항을 충족하지 못한 P의 멤버를 가리키는 "schemaPath"를 가져야 한다.

    • 인스턴스가 객체이면, P를 이름이 "properties"인 스키마 멤버의 값(존재하는 경우)이라고 하고, O를 이름이 "optionalProperties"인 스키마 멤버의 값(존재하는 경우)이라고 하자.

      인스턴스의 모든 멤버 I에 대해, P 또는 O 안에서 I의 이름과 같은 이름을 가진 멤버를 찾는다. Section 2.2.6에 따라 PO 모두가 그러한 멤버를 가질 수 없음을 안다. "discriminator tag exemption"이 I에 대해 유효하다면(Section 3.3.8 참조), I를 무시한다.

      그렇지 않으면:

      • P 또는 O 안에 그러한 멤버가 존재하지 않고, 검증이 "allow additional properties" 모드에 있지 않다면 (Section 3.1 참조), 스키마는 인스턴스를 거부한다.

        이 경우의 오류 표시자는 I를 가리키는 "instancePath"와 스키마를 가리키는 "schemaPath"를 가진다.

      • 그러한 멤버가 P 또는 O 안에 존재한다면, 이 멤버를 S라고 부른다. SI의 값을 거부하면, 스키마는 인스턴스를 거부한다.

        이 경우의 오류 표시자는 SI의 값에 대해 평가하여 나온 오류 표시자의 합집합이다.

  • 인스턴스가 객체이면, 위 목록의 두 번째와 세 번째 글머리 항목에서 발생하는 여러 오류를 가질 수 있다. 이 경우 오류 표시자는 오류들의 합집합이다.

    예를 들어, 스키마

       {
         "properties": {
           "a": { "type": "string" },
           "b": { "type": "string" }
         },
         "optionalProperties": {
           "c": { "type": "string" },
           "d": { "type": "string" }
         }
       }
    

    는 다음을 수락하고

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

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

       { "a": "foo", "b": "bar", "c": "baz", "d": "quux" }
    

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

    다음은 거부한다.

       null
    

    오류 표시자는 다음과 같다.

       [{ "instancePath": "", "schemaPath": "/properties" }]
    

    그리고 다음을 거부한다.

       { "b": 3, "c": 3, "e": 3 }
    

    오류 표시자는 다음과 같다.

       [
         { "instancePath": "",
           "schemaPath": "/properties/a" },
         { "instancePath": "/b",
           "schemaPath": "/properties/b/type" },
         { "instancePath": "/c",
           "schemaPath": "/optionalProperties/c/type" },
         { "instancePath": "/e",
           "schemaPath": "" }
       ]
    

    대신 스키마가 그 밖에는 같지만 "additionalProperties: true"를 가지고 있었다면:

       {
         "properties": {
           "a": { "type": "string" },
           "b": { "type": "string" }
         },
         "optionalProperties": {
           "c": { "type": "string" },
           "d": { "type": "string" }
         },
         "additionalProperties": true
       }
    

    그리고 인스턴스가 그대로였다면:

       { "b": 3, "c": 3, "e": 3 }
    

    그때 인스턴스를 스키마에 대해 평가하여 나온 오류 표시자는 다음과 같을 것이다.

       [
         { "instancePath": "",
           "schemaPath": "/properties/a" },
         { "instancePath": "/b",
           "schemaPath": "/properties/b/type" },
         { "instancePath": "/c",
           "schemaPath": "/optionalProperties/c/type" },
       ]
    

    이들은 최종 오류(인스턴스의 "e"라는 이름의 추가 멤버와 관련된 오류)가 더 이상 존재하지 않는 것을 제외하면 이전과 같은 오류들이다. 이는 "additionalProperties: true"가 스키마에서 "allow additional properties" 모드를 활성화하기 때문이다.

    마지막으로, 스키마

       {
         "nullable": true,
         "properties": {
           "a": { "type": "string" },
           "b": { "type": "string" }
         },
         "optionalProperties": {
           "c": { "type": "string" },
           "d": { "type": "string" }
         },
         "additionalProperties": true
       }
    

    는 다음을 수락하지만

       null
    

    다음은 거부한다.

       { "b": 3, "c": 3, "e": 3 }
    

    오류 표시자는 다음과 같다.

       [
         { "instancePath": "",
           "schemaPath": "/properties/a" },
         { "instancePath": "/b",
           "schemaPath": "/properties/b/type" },
         { "instancePath": "/c",
           "schemaPath": "/optionalProperties/c/type" },
       ]
    

3.3.7.

"values" 형식은 연관 배열로 사용되는 JSON 객체인 인스턴스를 설명하기 위한 것이다. "values" 형식의 구문은 Section 2.2.7에 설명되어 있다.

스키마가 "values" 형식이면 다음과 같다.

  • 스키마가 이름이 "nullable"이고 값이 boolean "true"인 멤버를 가지고 있으며, 인스턴스가 JSON 원시 값 "null"이면, 스키마는 그 인스턴스를 수락한다.

    그렇지 않으면:

    • S를 이름이 "values"인 스키마 멤버의 값이라고 하자. 인스턴스는 다음이 모두 참인 경우에 그리고 그 경우에만 수락된다.

      • 인스턴스는 객체이다. 그렇지 않으면, 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와 이름이 "values"인 스키마 멤버를 가리키는 "schemaPath"를 가져야 한다.
      • 인스턴스가 객체이면, 인스턴스의 모든 멤버 값은 S에 의해 수락되어야 한다. 그렇지 않으면, 이 경우의 오류 표시자는 S를 인스턴스의 멤버 값에 대해 평가하면서 발생한 모든 오류 표시자의 합집합이다.

예를 들어, 스키마

   {
     "values": {
       "type": "float32"
     }
   }

는 다음을 수락하고

   {}

   {"a": 1, "b": 2}

다음은 거부한다.

   null

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/values" }]

그리고 다음을 거부한다.

   { "a": 1, "b": 2, "c": "foo", "d": 3, "e": "bar" }

오류 표시자는 다음과 같다.

   [
     { "instancePath": "/c", "schemaPath": "/values/type" },
     { "instancePath": "/e", "schemaPath": "/values/type" }
   ]

스키마

   {
     "nullable": true,
     "values": {
       "type": "float32"
     }
   }

는 다음을 수락하지만

   null

다음은 거부한다.

   { "a": 1, "b": 2, "c": "foo", "d": 3, "e": "bar" }

오류 표시자는 다음과 같다.

   [
     { "instancePath": "/c", "schemaPath": "/values/type" },
     { "instancePath": "/e", "schemaPath": "/values/type" }
   ]

3.3.8. 판별자

"discriminator" 형식은 C 계열 언어의 discriminated union 구성과 유사한 방식으로 사용되는 JSON 객체를 설명하기 위한 것이다. "discriminator" 형식의 구문은 Section 2.2.8에 설명되어 있다.

스키마가 "discriminator" 형식이면, 다음을 검증한다.

  • 인스턴스가 객체인지,
  • 인스턴스가 특정 "tag" 속성을 가지는지,
  • 이 "tag" 속성의 값이 유효한 값 집합 안의 문자열인지, 그리고
  • 인스턴스가 다른 스키마를 만족하는지. 여기서 이 다른 스키마는 "tag" 속성의 값에 기반해 선택된다.

"discriminator" 형식의 동작은 다른 키워드보다 더 복잡하다. CDDL에 익숙한 독자는 부록 B의 마지막 예제가 그 동작을 이해하는 데 도움이 될 수 있다. 이 절의 다음 내용은 "discriminator" 형식의 동작 설명과 몇 가지 예제이다.

스키마가 "discriminator" 형식이면 다음과 같다.

  • D를 이름이 "discriminator"인 스키마 멤버라고 하자.
  • M을 이름이 "mapping"인 스키마 멤버라고 하자.
  • I를 그 이름이 D의 값과 같은 인스턴스 멤버라고 하자. I는 일부 거부된 인스턴스의 경우 존재하지 않을 수 있다.
  • S를 그 이름이 I의 값과 같은 M의 멤버라고 하자. S는 일부 거부된 인스턴스의 경우 존재하지 않을 수 있다.

스키마가 이름이 "nullable"이고 값이 boolean "true"인 멤버를 가지고 있으며, 인스턴스가 JSON 원시 값 "null"이면, 스키마는 그 인스턴스를 수락한다. 그렇지 않으면, 인스턴스는 다음이 모두 참인 경우에 그리고 그 경우에만 수락된다.

  • 인스턴스가 객체이다.

    그렇지 않으면, 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와 D를 가리키는 "schemaPath"를 가져야 한다.

  • 인스턴스가 JSON 객체이면, I가 존재해야 한다.

    그렇지 않으면, 이 경우의 오류 표시자는 인스턴스를 가리키는 "instancePath"와 D를 가리키는 "schemaPath"를 가져야 한다.

  • 인스턴스가 JSON 객체이고 I가 존재한다면, I의 값은 문자열이어야 한다.

    그렇지 않으면, 이 경우의 오류 표시자는 I를 가리키는 "instancePath"와 D를 가리키는 "schemaPath"를 가져야 한다.

  • 인스턴스가 JSON 객체이고 I가 존재하며 문자열 값을 가진다면, S가 존재해야 한다.

    그렇지 않으면, 이 경우의 오류 표시자는 I를 가리키는 "instancePath"와 M을 가리키는 "schemaPath"를 가져야 한다.

  • 인스턴스가 JSON 객체이고, I가 존재하며, S가 존재한다면, 인스턴스는 S의 값을 만족해야 한다. Section 2에 따라, S의 값이 "properties" 형식의 스키마임을 안다. 인스턴스가 S의 값을 만족하는지 평가할 때 Section 3.3.6에서 제공되는 "discriminator tag exemption"을 I에 적용한다.

    그렇지 않으면, 이 경우의 오류 표시자는 "discriminator tag exemption"을 I에 적용한 상태에서 S의 값을 인스턴스에 대해 평가하여 나온 오류 표시자여야 한다.

위 목록 항목들은 상호 배타적인 방식으로 정의되어 있다. 주어진 인스턴스와 스키마에 대해, 위 목록 항목 중 정확히 하나가 적용된다.

예를 들어, 스키마

   {
     "discriminator": "version",
     "mapping": {
       "v1": {
         "properties": {
           "a": { "type": "float32" }
         }
       },
       "v2": {
         "properties": {
           "a": { "type": "string" }
         }
       }
     }
   }

는 다음을 거부한다.

   null

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/discriminator" }]

(이는 인스턴스가 객체가 아닌 경우이다.)

또한 거부되는 것은 다음이다.

   {}

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/discriminator" }]

(이는 I가 존재하지 않는 경우이다.)

또한 거부되는 것은 다음이다.

   { "version": 1 }

오류 표시자는 다음과 같다.

   [
     {
       "instancePath": "/version",
       "schemaPath": "/discriminator"
     }
   ]

(이는 I가 존재하지만 문자열 값을 가지지 않는 경우이다.)

또한 거부되는 것은 다음이다.

   { "version": "v3" }

오류 표시자는 다음과 같다.

   [
     {
       "instancePath": "/version",
       "schemaPath": "/mapping"
     }
   ]

(이는 I가 존재하고 문자열 값을 가지지만 S가 존재하지 않는 경우이다.)

또한 거부되는 것은 다음이다.

   { "version": "v2", "a": 3 }

오류 표시자는 다음과 같다.

   [
     {
       "instancePath": "/a",
       "schemaPath": "/mapping/v2/properties/a/type"
     }
   ]

(이는 IS가 존재하지만 인스턴스가 S의 값을 만족하지 않는 경우이다.)

마지막으로, 스키마는 다음을 수락한다.

   { "version": "v2", "a": "foo" }

이 인스턴스는 "version"이 "/mapping/v2/properties"에 언급되어 있지 않더라도 수락된다. "discriminator tag exemption"은 인스턴스를 S의 값에 대해 평가할 때 "version"이 추가 속성으로 처리되지 않도록 보장한다.

반대로, 같은 스키마이지만 "nullable"이 "true"인 경우를 고려하라. 스키마

   {
     "nullable": true,
      "discriminator": "version",
      "mapping": {
        "v1": {
          "properties": {
            "a": { "type": "float32" }
          }
        },
        "v2": {
          "properties": {
            "a": { "type": "string" }
          }
        }
      }
   }

는 다음을 수락한다.

   null

"discriminator" 형식을 예제로 더 설명하기 위해, Section 2.2.8의 JTD 스키마를 떠올려 보자. 여기 다시 제시한다.

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

이 스키마는 다음을 수락하고

   { "event_type": "account_deleted", "account_id": "abc-123" }

   {
     "event_type": "account_payment_plan_changed",
     "account_id": "abc-123",
     "payment_plan": "PAID"
   }

   {
     "event_type": "account_payment_plan_changed",
     "account_id": "abc-123",
     "payment_plan": "PAID",
     "upgraded_by": "users/mkhwarizmi"
   }

다음은 거부한다.

   {}

오류 표시자는 다음과 같다.

   [{ "instancePath": "", "schemaPath": "/discriminator" }]

그리고 다음을 거부한다.

   { "event_type": "some_other_event_type" }

오류 표시자는 다음과 같다.

   [
     {
       "instancePath": "/event_type",
       "schemaPath": "/mapping"
     }
   ]

그리고 다음을 거부한다.

   { "event_type": "account_deleted" }

오류 표시자는 다음과 같다.

   [{
     "instancePath": "",
     "schemaPath": "/mapping/account_deleted/properties/account_id"
   }]

그리고 다음을 거부한다.

   {
     "event_type": "account_payment_plan_changed",
     "account_id": "abc-123",
     "payment_plan": "PAID",
     "xxx": "asdf"
   }

오류 표시자는 다음과 같다.

   [{
     "instancePath": "/xxx",
     "schemaPath": "/mapping/account_payment_plan_changed"
   }]

4. IANA 고려사항

이 문서에는 IANA 조치가 없다.

5. 보안 고려사항

JTD 구현은 필연적으로 JSON 데이터를 조작하게 된다. 따라서 [RFC8259]의 보안 고려사항은 모두 여기에 관련된다.

사용자가 입력한 스키마를 평가하는 구현은 순진한 구현을 무한 루프에 빠뜨릴 수 있는 순환 참조를 감지하고 중단하는 메커니즘을 구현해야 SHOULD 한다. 그러한 메커니즘이 없으면, 구현은 서비스 거부 공격에 취약할 수 있다.

6. 참고문헌

6.1. 규범 참고문헌

[RFC2119]
Bradner, S., "요구사항 수준을 나타내기 위해 RFC에서 사용하는 핵심 단어", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/info/rfc2119>.
[RFC3339]
Klyne, G. and C. Newman, "인터넷의 날짜와 시간: 타임스탬프", RFC 3339, DOI 10.17487/RFC3339, , <https://www.rfc-editor.org/info/rfc3339>.
[RFC4287]
Nottingham, M., Ed. and R. Sayre, Ed., "Atom 신디케이션 형식", RFC 4287, DOI 10.17487/RFC4287, , <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, , <https://www.rfc-editor.org/info/rfc6901>.
[RFC8174]
Leiba, B., "RFC 2119 핵심 단어에서 대문자와 소문자의 모호성", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/info/rfc8174>.
[RFC8259]
Bray, T., Ed., "JavaScript Object Notation (JSON) 데이터 교환 형식", STD 90, RFC 8259, DOI 10.17487/RFC8259, , <https://www.rfc-editor.org/info/rfc8259>.
[RFC8610]
Birkholz, H., Vigano, C., and C. Bormann, "Concise Data Definition Language(CDDL): Concise Binary Object Representation(CBOR) 및 JSON 데이터 구조를 표현하기 위한 표기 규약", RFC 8610, DOI 10.17487/RFC8610, , <https://www.rfc-editor.org/info/rfc8610>.

6.2. 정보 참고문헌

[JSON-SCHEMA]
Wright, A., Andrews, H., Hutton, B., and G. Dennis, "JSON Schema: JSON 문서를 설명하기 위한 미디어 타입", 진행 중인 작업, Internet-Draft, draft-handrews-json-schema-02, , <https://tools.ietf.org/html/draft-handrews-json-schema-02>.
[OPENAPI]
OpenAPI Initiative, "OpenAPI 명세", , <https://spec.openapis.org/oas/v3.0.3>.
[RFC7071]
Borenstein, N. and M. Kucherawy, "평판 교환을 위한 미디어 타입", RFC 7071, DOI 10.17487/RFC7071, , <https://www.rfc-editor.org/info/rfc7071>.
[RFC7493]
Bray, T., Ed., "I-JSON 메시지 형식", RFC 7493, DOI 10.17487/RFC7493, , <https://www.rfc-editor.org/info/rfc7493>.

부록 A. 생략된 기능에 대한 근거

이 부록은 규범적이지 않다.

이 절은 JSON Type Definition에서 의도적으로 제외된 가능한 기능을 설명하고, 이러한 기능이 생략된 이유를 정당화한다.

A.1. 64비트 숫자 지원

이 문서는 "int64" 또는 "uint64"를 JTD "type" 키워드의 값으로 허용하지 않는다(2.2.3절 및 3.3.3절 참조). 그러한 가상의 "int64" 또는 "uint64" 타입은 "int32" 또는 "uint32"처럼(각각) 동작하되, 32비트 정수 대신 64비트와 관련된 값 범위를 가질 것이다. 즉:

  • "int64"는 -(2**63)과 (2**63)-1 사이의 숫자를 수락할 것이다
  • "uint64"는 0과 (2**64)-1 사이의 숫자를 수락할 것이다

"int64" 및 "uint64" 사용자는 부호 있는 또는 부호 없는 64비트 정수의 전체 범위가 정밀도 손실 없이 JSON으로 상호운용 가능하게 전송될 수 있다고 기대할 가능성이 높다. 그러나 이러한 가정은 Section 2.2 of [RFC7493]에 제시된 이유로 인해 틀렸을 가능성이 높다.

"int64"와 "uint64"는 64비트 정수의 전체 범위를 정밀도 손실 없이 JSON으로 상호운용 가능하게 처리할 수 있다고 사용자가 잘못 가정하게 만들 가능성이 높았을 것이다. 사용자를 잘못 이끌지 않기 위해, JTD는 "int64"와 "uint64"를 생략한다.

A.2. 비루트 정의 지원

이 문서는 "definitions" 키워드가 루트 스키마 밖에 나타나는 것을 허용하지 않는다(그림 1 참조). 생각해 보면, 이 문서는 대신 "definitions"가 비루트 스키마를 포함한 모든 스키마에 나타나는 것을 허용할 수도 있었다. 이 대안 설계에서 "ref"는 "ref"를 포함하고 적절한 이름의 "definitions" 멤버를 가진 "가장 가까운"(즉, 가장 깊이 중첩된) 스키마의 정의로 해석될 것이다.

예를 들어, 이 대안 접근 방식에서는 그림 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"
    }
  }
}
그림 3: 이 문서가 비루트 정의를 허용했다면 가능했을 가상 스키마. 이는 올바른 JTD 스키마가 아니다.

그림 3의 스키마와 같은 것이 허용된다면, JTD 스키마로부터의 코드 생성은 더 어려워지고, 생성된 코드는 덜 유용할 것이다.

코드 생성은 더 어려워질 것이다. 이는 코드 생성기가 정의로부터 생성된 타입에 대해 이름 맹글링 체계를 구현하도록 강제하기 때문이다. 이러한 추가 어려움은 엄청나지는 않지만, 그렇지 않으면 비교적 사소한 작업에 복잡성을 더한다.

생성된 코드는 덜 유용할 것이다. 생성되고 맹글링된 struct 이름은 사람이 정의한 struct 이름보다 덜 간결하기 때문이다. 예를 들어, 그림 3의 "user" 정의는 "PropertiesFooUser", "PropertiesBarUser", "PropertiesBazUser"라는 이름의 타입으로 생성되었을 수 있다. 이와 같은 난해한 이름은 "User"와 같은 이름보다 사람이 작성한 코드에 덜 유용하다.

또한 "PropertiesFooUser"와 "PropertiesBarUser"가 본질적으로 동일하더라도, 많은 정적 타입 프로그래밍 언어에서는 서로 바꿔 쓸 수 없을 것이다. 코드 생성기는 동일한 정의를 중복 제거하여 이를 우회하려고 시도할 수 있지만, 그러면 사용자는 "user_id"가 아니라 "userId"라는 이름의 속성을 허용하는 스키마에서 정의된 미묘하게 다른 "PropertiesBazUser"가 왜 중복 제거되지 않았는지 혼란스러워할 수 있다.

비루트 정의와 관련된 구현 및 사용성 과제가 있어 보이고, 나중에 JTD를 개정하여 비루트 정의를 허용하는 것이, 나중에 JTD를 개정하여 이를 금지하는 것보다 쉬울 것이기 때문에, 이 문서는 JTD 스키마에서 비루트 정의를 허용하지 않는다.

부록 B. CDDL과의 비교

이 부록은 규범적이지 않다.

CDDL에 익숙한 독자를 돕기 위해, 이 절은 같은 인스턴스를 수락하고 거부하는 JTD 스키마와 CDDL 스키마를 제시하여 JTD가 어떻게 작동하는지 보여준다.

JTD 스키마

   {}

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = any

JTD 스키마

   {
     "definitions": {
       "a": { "elements": { "ref": "b" }},
       "b": { "type": "float32" }
     },
     "elements": {
       "ref": "a"
     }
   }

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = [* a]
   a = [* b]
   b = number

JTD 스키마

   { "enum": ["PENDING", "DONE", "CANCELED"]}

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = "PENDING" / "DONE" / "CANCELED"

JTD 스키마

   {"type": "boolean"}

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = bool

JTD 스키마:

   {"type": "float32"}

   {"type": "float64"}

는 둘 다 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = number

JTD 스키마

   {"type": "string"}

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = tstr

JTD 스키마

   {"type": "timestamp"}

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = tdate

JTD 스키마

   { "elements": { "type": "float32" }}

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = [* number]

JTD 스키마

   {
     "properties": {
       "a": { "type": "boolean" },
       "b": { "type": "float32" }
     },
     "optionalProperties": {
       "c": { "type": "string" },
       "d": { "type": "timestamp" }
     }
   }

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = { a: bool, b: number, ? c: tstr, ? d: tdate }

JTD 스키마

   { "values": { "type": "float32" }}

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = { * tstr => number }

마지막으로, JTD 스키마

   {
     "discriminator": "a",
     "mapping": {
       "foo": {
         "properties": {
           "b": { "type": "float32" }
         }
       },
       "bar": {
         "properties": {
           "b": { "type": "string" }
         }
       }
     }
   }

는 CDDL 규칙과 같은 인스턴스를 수락한다.

   root = { a: "foo", b: number } / { a: "bar", b: tstr }

부록 C. 예제

이 부록은 규범적이지 않다.

JTD의 시연으로, 그림 4에는 Section 6.2.2 of [RFC7071]에 설명된 평이한 영어 정의 "reputation-object"와 매우 동등한 JTD 스키마가 있다.

{
  "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" }
        }
      }
    }
  }
}
그림 4: Section 6.2.2 of [RFC7071]의 "reputation-object"를 설명하는 JTD 스키마

이 스키마는 "sample-size", "generated", "expires"가 무한한 양의 정수여야 한다는 요구사항을 강제하지 않는다. 또한 "rating", "confidence", "normal-rating"이 소수점 이하 세 자리보다 더 많은 정밀도를 가져서는 안 된다는 제한을 표현하지 않는다.

그림 4의 예제는 Appendix H of [RFC8610]의 동등한 예제와 비교할 수 있다.

감사의 말

Carsten Bormann은 JTD 설계와 이 문서의 구조에 대해 많은 유용한 지침과 피드백을 제공했다.

Evgeny Poberezkin은 "nullable" 추가를 제안했으며, 실수와 단순화 기회를 찾기 위해 이 문서를 철저히 검토했다.

Tim Bray는 현재의 "ref" 모델과 "enum" 추가를 제안했다. Anders Rundgren은 "type"이 숫자 타입을 더 많이 지원하도록 확장할 것을 제안했다. James Manger는 정수 타입이 어떻게 작동하는지에 대한 추가적인 명확화 예제를 제안했다. Adrian Farrel은 이 문서를 더 명확하게 만드는 데 도움이 되는 많은 개선을 제안했다.

IETF JSON 메일링 리스트의 구성원들, 특히 Pete Cordell, Phillip Hallam-Baker, Nico Williams, John Cowan, Rob Sayre, 그리고 Erik Wilde는 많은 유용한 피드백을 제공했다.

OpenAPI의 "discriminator" 객체 [OPENAPI]는 "discriminator" 형식에 영감을 주었다. [JSON-SCHEMA]는 JTD 초기 설계의 여러 부분에 영향을 주었다.

저자 주소

Ulysse Carion
Segment.io, Inc
100 California Street
San Francisco, CA 94111
미국