jxn0.2

jxn 0.2 명세

jxn(JSON eXplained Notation)은 JSON 의 형태와 뜻을 정의하는 언어(스키마 언어)다 -- 사람이 읽는 정의에 뜻과 관계를 적고, JSON Schema 로 옮긴다. 이 문서는 jxn 0.2 의 명세다. 2026-09-27 확정. 1.0 전(0.x)에는 뒤로 깨는 변경이 있을 수 있다 -- 바뀐 것은 CHANGELOG.md 에 적는다. 0.2 는 뼈대를 JSON 처럼 쓴다 -- 정의를 보면 인스턴스의 모양이 보인다. 뜻 · 주석 · 관계는 키의 옆과 아래에 붙는다. 결정은 notes/문법-0.2-결정.md.

한 줄 원칙: { } 는 object, [ ] 는 배열, | 줄은 갈래와 값 목록, 따옴표는 인스턴스에 나오는 키와 값, 맨 낱말은 타입, // 는 뜻, @ 는 주석, 관계 낱말로 시작하는 줄은 관계.

0. 한눈에#

파일로도 있다 -- examples/order/order.jxn (인스턴스 order.json).

jxn
jxn 0.2

// # 목록과 주석

list owner                                  // 닫힌 목록 (정의 파일이 스스로 선언한다)
    | customer                              // 고객이 넣은 값
    | system                                // 시스템이 계산한 값
    | warehouse                             // 창고 시스템이 돌려준 원문
tag owner : set<owner>  inherit need        // @owner 주석. 조상에서 이어받고, 모든 키에 있어야 한다
tag status need                             // jxn 이 주는 status 를 모든 키에 요구한다

anchor change = $.history : Change          // 이웃 주소 change[-1] = 바로 앞 Change (Note 는 건너뛴다)
anchor catalog external                     // 인스턴스 밖의 것

// # 주문

{                                           // 주문 하나
    @owner system  @status stable
    "id": string,                           // 주문 번호
        @status stable
    "items": [ Item ] min 1,                // 담은 상품
        @owner customer  @status stable
    "total": number,                        // 합계 금액
        @status stable
        derived-from items[*].price         // 담은 상품 값의 합
    "method": "card" | "cash",              // 결제 방법
        @status stable
    "payment"?: by method                   // 결제 -- 방법이 모양을 정한다
        @status draft
        | "card" => { "last4": string }     // 카드 끝 네 자리
            @status draft
            // 한 줄 object 의 키(last4)는 이 갈래의 주석을 쓴다
        | "cash" => absent                  // 현금이면 없다
    "history": [ Change | Note ]            // 주문 이력
        @status stable
        // 고치지 않고 붙이기만 한다. 갈래는 어느 키가 있나로 갈린다
}

def Item {                                  // 담은 상품 하나
    @status stable
    "sku": string,                          // 상품 코드
        @status stable
        refers-to catalog.items
    "price": number                         // 담을 때의 값
        @status stable
}

def Change {                                // 단계가 바뀐 기록
    @status stable
    "from": State,                          // 바뀌기 전 단계
        @status stable
        equals change[-1].to                // 바로 앞 변경의 도착 단계에서 출발한다
    "to": State                             // 바뀐 단계
        @status stable
}

def Note {                                  // 사람이 남긴 메모
    @owner customer  @status draft
    "text": string                          // 메모 글
        @status draft
}

def State = "open" | "paid" | "shipped"     // 주문의 단계
    @status stable

같은 모양의 인스턴스:

json
{
  "id": "A-1",
  "items": [{ "sku": "P-10", "price": 1200 }, { "sku": "P-11", "price": 300 }],
  "total": 1500,
  "method": "card",
  "payment": { "last4": "4242" },
  "history": [{ "from": "open", "to": "paid" }, { "text": "포장 부탁" }]
}

1. 파일과 줄 -- 문법은 줄 단위다#

줄 꼴 뜻
머리 jxn 0.2 표기의 판. 첫 코드 줄이다
뿌리 { ... } 인스턴스의 뿌리 object (2-3)
키 "key": T · "key"?: T · "key": by k · "key": 이름이 붙는 자리 (2-4)
선언 def N ... · list N · tag N ... · anchor N ... · home 주소 맨 위 수준
갈래 · 값 | "v" => T · | "v" · | 낱말 위 선언의 갈래 · 값 목록 · 닫힌 목록 (2-1 · 2-6 · 2-8)
주석 @tag 값[, 값] [@tag 값 ...] 바로 앞 선언에 붙는다. 한 줄에 여럿
관계 [.sub] 낱말 주소 [when 조건] 바로 앞 선언이 주어다 (2-11)
펼치기 ...Name Name 의 키를 이 object 에 펼친다
닫기 } · }] · } } 블록을 닫는다. 뒤의 글자는 열 때의 타입식을 마저 닫는다 ([{ ... }])
말 // 글 (줄 전체) 풀이. 바로 앞 선언(또는 머리)에 붙는다
머리 // # 글 (줄 전체) 무리를 가른다 -- 같은 블록 · 목록에서 다음 머리까지의 형제를 묶는다. 맨 위 수준이면 절
물음 // ? 글 (줄 전체) 열린 물음. 바로 앞 선언에 붙고, 도구가 목록으로 모은다
꾸밈 // ==== 처럼 / - = 로만 된 줄 버린다

2. 구성 -- 꼴 · 뜻 · 예 · JSON Schema#

2-1. 닫힌 목록 list#

text
list owner                      // 누가 채우나
    | customer                  // 고객이 넣은 값
    | "external api"            // 공백이 있으면 따옴표
    | 0                         // 수도 된다

2-2. 주석 선언 tag#

text
tag note    : text                          // 글
tag src     : set<text>                     // 글 여럿
tag owner   : set<owner>  inherit need      // 목록의 값 하나 이상
tag audience: set<role> | all  inherit need // 목록 값들, 또는 낱말 all 홀로
tag stage   : stage  inherit                // 목록의 값 하나
tag status need                             // jxn 이 준 주석에 방식만 더한다 (2-15)

2-3. 뿌리 { } 와 def#

text
{                                   // 인스턴스의 뿌리 -- 파일에 하나. 이름이 없다
    "id": string
}
def Item {                          // 이름 붙은 object
    "sku": string
}
def Address { "street": string, "city": string }   // 한 줄 object
def Method = "card" | "cash"        // 이름 붙은 타입식
def Money = number min 0            // 주석 · 관계 줄이 뒤따를 수 있다
    @status stable
def State =                         // 값마다 뜻을 다는 값 목록 (2-6)
    | "open"                        // 담는 중
    | "paid"                        // 결제됨

2-4. 키 -- 늘 있다 · 없을 수 있다 · null 일 수 있다#

text
"id": string                  // 늘 있다                  -> required
"note"?: string               // 없을 수 있다             -> required 에 넣지 않는다
"coupon": null | Coupon       // 늘 있되 null 일 수 있다  -> required + anyOf [null, ...]
"ship to": Address            // 키 글자는 JSON 문자열 -- 공백 · 한글도 된다

2-5. 타입식#

꼴 뜻 JSON Schema
string integer number boolean 기본 type
any 무엇이든 -- 일부러 열었다 {}
unknown 타입을 아직 모른다 {} + x-jxn-unknown: true
"a" | "b" · 0 · true 허용 값 -- 따옴표는 인스턴스에 나오는 실제 값이다 enum (전부 값 · null 이면) · 아니면 anyOf
A | B 둘 중 하나 anyOf (object def 둘 이상이면 2-9)
[ T ] 배열 type: array, items
{ [키의 뜻]: T } 키가 열린 object (map). 어떤 키든, 그 키는 [ ] 안의 뜻 additionalProperties: T + x-jxn-key
{ ... } · {} 닫힌 object -- 적힌 키만 additionalProperties: false (갈래 · 펼치기가 있으면 unevaluatedProperties: false)
{ "a": T, "b"?: U } 한 줄 object -- 쉼표로 가른다. 안쪽 키에는 뜻 · 주석 · 관계를 못 단다 -- 그 object 가 놓인 자리(키 · 갈래 · 쓰는 자리)의 주석을 이어받지 않는 것까지 그대로 쓴다 위와 같다
T min n · T max n 범위 수 minimum · 배열 minItems · 글 minLength · map minProperties (max 도 같다)
expect T 기대하는 모양 (2-10) x-jxn-expect: <T> -- 표준 검사기는 보지 않는다
absent 이 갈래에서는 키가 없다 (갈래에서만) properties: {key: false}
text
"lines": [{                     // 여러 줄 object 의 배열은 [{ 로 열고 }] 로 닫는다
    "sku": string,
    "qty": integer min 1
}] min 1,
"stock": { [sku]: integer },    // 상품 코드마다 재고
"values": { [value id]: {       // 여러 줄 object 의 map 은 } } 로 닫는다
    "value": any
} }

2-6. 값마다 뜻을 다는 값 목록 |#

text
"state":                        // 주문의 단계
    @status stable
    // 반품은 다음 판에서 정한다      (state 의 풀이 -- 첫 | 줄 앞)
    // # 담는 동안
    | "open"                    // 담는 중
    | "held"   @since 2         // 잠시 멈춤
    // # 값을 치른 뒤
    | "paid"   @since 2         // 결제됨
        // 부분 결제는 아직 없다      (paid 의 풀이)
    | "shipped"  @status draft  // 보냄

2-7. 펼치기 ...Name#

text
def Address { "street": string, "city": string }
"shipTo": {
    ...Address,
    "note"?: string             // Address 에 키 하나를 더한다
}

2-8. 갈래 by#

키 하나의 모양을 이웃 키가 정한다 (필드 갈래):

text
"method": "card" | "cash" | "voucher",
"payment"?: by method                   // 결제 내용 -- method 의 값에 따라
    | "card" => { "last4": string }     // 카드 끝 네 자리
    | "voucher" => Voucher
    | "cash" => absent                  // 현금이면 payment 키가 없다

object 의 키 집합을 한 키가 정한다 (object 갈래):

text
def Discount {
    "kind": "percent" | "fixed"
    by kind
        | "percent" => { "rate": number }   // 이 갈래에서만 rate 가 있다
        | "fixed"   => { "amount": number }
}

2-9. 키가 있나로 갈리는 합 A | B#

text
"history": [ Change | Note ]        // Change 는 from 이, Note 는 text 가 있다

2-10. 기대하는 모양 expect T#

text
"reply": expect StockReply,         // 창고가 돌려준 원문. 모양이 어긋나도 그대로 적힌다
"replyCheck": [ string ]            // 어긋난 칸 -- 어긋남은 여기가 말한다

2-11. 관계#

키와 키 사이의 뜻 -- "이 값은 저 값에서 계산된다", "이 값은 저 목록의 식별자다" -- 를 적는다. 관계는 한 번만, 주어 쪽에 적는다.

text
"total": number,                            // 합계 금액
    derived-from items[*].price             // 담은 상품 값의 합
    derived-from discount.amount  when discount.kind == "fixed"   // 정액 할인을 뺀다
조각 뜻
줄의 꼴 [.sub] 낱말 주소 [when 조건] // note -- 키(또는 갈래 · 값) 아래에 들여 쓴다
주어 바로 위의 선언. .sub 가 있으면 그 아래 키로 좁힌다 (.carrier refers-to catalog.carriers -- 공유 def 를 쓰는 자리에서 안쪽 키의 관계를 적을 때)
낱말 아래 열하나 -- jxn 이 준다
과녁 주소 (2-12)
when 이 관계가 서는 조건 (2-13). 갈래 안이면 갈래의 조건이 저절로 더해진다
note 줄 끝 // -- 그 관계의 풀이
낱말 문장 틀 거꾸로 읽기 (과녁 쪽)
equals 주어는 과녁과 같다 같다
member-of 주어(값)는 과녁(배열)의 원소 · 주어(배열)는 과녁의 부분 담는다
refers-to 주어의 값이 과녁의 식별자다 가리켜진다
follows 주어(원소)는 과녁 바로 뒤에 붙는다 앞에 선다
copies 과녁의 값을 모양만 바꿔 옮겨 온다 옮겨 간다
derived-from 과녁을 재료로 계산된다 재료다
sets · clears 주어가 붙으면 과녁 상태가 켜진다 · 꺼진다 켜진다 · 꺼진다
adds-to · removes-from 주어(가 가리키는 원소)가 과녁 모음(배열 · map)에 들어간다 · 빠진다 넣어진다 · 빠진다
reason-of 주어는 과녁의 까닭이다 까닭을 가진다

2-12. 주소#

text
주소  := "$" 경로 | 앵커 [ 경로 ] | 이름 경로       $ 는 뿌리, 앵커가 없으면 home 에서
경로  := ( "." 이름 | "[*]" | ".*" )*               [*] = 배열의 모든 원소 · .* = map 의 모든 값
앵커  := 이름 [ "[" ("+n" | "-n" | "*" | "..-n") "]" ]

선언:

text
home   $.history[*]                     // 앵커 없는 주소 = 주어가 든 이력 원소에서
anchor history = $.history              // history[+1] · history[-1] = 바로 옆 원소
anchor change  = $.history : Change     // change[-1] = 바로 앞 Change (다른 갈래는 건너뛴다)
anchor catalog external                 // 인스턴스 밖 -- 검사하려면 함께 받아야 한다

2-13. 조건 when#

text
when   := 절 (" and " 절)*
절     := 주소 op 값
op     := "==" | "!=" | "in"
값     := JSON 값 | enum def 이름 (in 의 오른쪽)

예: when method == "card" · when state in ["paid", "shipped"] · when method in Method.

2-14. 이어받기#

2-15. 상태 status -- jxn 이 준다#

값 뜻
stable 정함 -- 뜻이 있고, 모양이 닫혔고, 정한 까닭이 있다
draft 초안 -- 키는 있으나 뜻이 아직 무르거나, 안쪽 모양 · 자리가 뒤로 미뤄졌다
stub 모름 -- 이름만 있다. 뜻(//)이 없어도 된다

3. 한 표로 -- JSON Schema 로#

jxn JSON Schema 2020-12
뿌리 { } 문서 뿌리 + title (파일 이름)
def N $defs/N · 쓰는 곳 $ref
"key": T / "key"?: T properties.key + required / required 에서 뺀다
{ ... } type: object, additionalProperties: false
{ ... } + 갈래 · 펼치기 unevaluatedProperties: false + allOf
...N N 의 properties · required · allOf 를 풀어 넣는다
by k + | v => T (필드) allOf: [{if: {k in [v]}, then: {key: T, required}}] · absent -> {key: false}
by k + | v => {...} (object) allOf: [{if: {k in [v]}, then: {properties}}]
A | B enum (모두 값) / anyOf
값 목록 | v // 뜻 @t x enum + x-jxn-values
[ T ] type: array, items: T
{ [뜻]: T } additionalProperties: T + x-jxn-key
unknown / any {x-jxn-unknown: true} / {}
expect T x-jxn-expect: T (표준 검사기는 보지 않는다)
T min n minimum / minItems / minLength / minProperties
줄 끝 // 뜻 description
말 줄 (풀이) x-jxn-notes: [...]
@status v x-jxn-status
@t v (파일이 선언한 주석) x-t
관계 x-relations
// ? · // # (나가지 않는다 -- jxn doc 의 열린 물음 목록 · 무리)

jxn 은 인스턴스에서 아무것도 추론하지 않는다 -- required 도 적힌 대로다. 예시 JSON 은 jxn doc 이 보여 주기에만 쓴다.

4. 오류#

아래는 틀린 파일이다 -- 도구는 JSON Schema 를 내지 않는다. 괄호 안은 참조 구현(jxn build)의 진단 코드다.

무엇 오류
첫 코드 줄이 jxn 0.2 가 아니다 E_HEADER
0.1 파일이다 (jxn 0.1) -- jxn upgrade 로 옮긴다 E_VERSION
읽을 수 없는 줄 · 타입식 · 주소 · 조건, 따옴표 없는 키 · 값, 붙을 선언이 없는 주석 · 관계 줄, 이름[주소], [] (0.2 는 [*]) E_SYNTAX
닫히지 않은 블록 E_UNCLOSED
뿌리가 없다 · 둘이다 E_NO_ROOT · E_ROOT_DUP
같은 이름의 def · list · tag · anchor, 한 object 의 같은 키, 한 목록의 같은 값 E_DUP · E_DUP_KEY · E_DUP_VALUE
없는 def · 목록을 부른다, object def 가 아닌 것을 펼친다 E_UNKNOWN_DEF · E_UNKNOWN_LIST · E_SPREAD
status 를 목록 · 타입으로 선언한다, status 를 이어받게 한다 E_STATUS_BUILTIN
값의 꼴이 없는 tag, 선언하지 않은 주석, 꼴에 맞지 않는 주석 값 E_TAG_NOSPEC · E_TAG_UNKNOWN · E_TAG_VALUE
need 인 주석이 이어받기를 푼 뒤에도 없는 키 E_NEED
갈래가 판별 키의 값을 덮지 않는다 · 남는다 · 겹친다, 판별 키가 없거나 값 목록이 아니다 E_BY_MISSING · E_BY_EXTRA · E_BY_DUP · E_BY_KEY
필드 갈래가 아닌 곳의 absent, absent 갈래가 있는데 ? 가 없는 키, object 가 아닌 object 갈래 E_ABSENT_PLACE · E_ABSENT_REQUIRED · E_BY_OBJECT
가르는 "반드시 있는 키" 가 없는 object 의 합 E_UNION_AMBIGUOUS
풀리지 않는 앵커 · home, 쓰는 자리에서 풀리지 않는 관계 · 조건의 주소, 값 목록이 아닌 in Def E_ANCHOR · E_ADDR · E_WHEN_ENUM

경고 -- 파일은 옳고 도구가 알린다.

무엇 경고
판이 0.2 가 아니다 (0.1 은 오류) W_VERSION
주석 줄 끝의 // 글 (버린다) W_DESC_IGNORED
한 선언에 같은 주석을 두 번 (뒤의 것이 이긴다) W_TAG_DUP
값에 단 관계 (아직 검사하지 않는다) W_REL_ON_VALUE

5. 0.2 에 없는 것#

쓰는 곳이 없거나 한 곳이다 -- 쓰임이 요구할 때 연다 (INTENT 7절).

6. 0.1 에서 옮기기#

sh
jxn upgrade FILE.jxn --write        # 0.1 파일을 0.2 로 덮어쓴다 (-o OUT.jxn 이면 다른 파일로, 없으면 stdout)