JSON RFC 8259: 프로덕션 파서를 넘어뜨리는 미묘한 세부
JSON은 당신 생각보다 작다
RFC 8259의 JSON 문법은 아주 작습니다: 객체, 배열, 문자열, 숫자, 그리고 세 리터럴 true, false, null. 이 단순함이 그것이 어디에나 있는 이유이며 — 또한 두 가지 놀라움이 사람들의 허를 찌르는 이유이기도 합니다. 첫째, 어떤 값이든 문서 전체가 될 수 있습니다: 42, "hello", true는 각각 완전하고 유효한 JSON입니다. 더 오래된 RFC 4627은 최상위가 객체나 배열이어야 한다고 요구했으므로, 일부 레거시 도구는 아직 맨 값을 거부합니다.
둘째, 명세는 여러 동작을 의도적으로 미지정으로 둡니다. 문법은 정밀하게 정의하지만 중복 키, 숫자 정밀도, 순서에 대해서는 거의 말하지 않습니다. 프로덕션 파서는 이 빈틈을 서로 다르게 메우며, 상호운용성 버그는 바로 거기에 삽니다.
중복 키: 미정의 동작
RFC 8259는 객체 이름이 고유해야 한다(SHOULD)고 말하지, 고유해야만 한다(MUST)고 말하지 않습니다. 키가 반복되면 결과는 미지정이라 파서가 갈립니다: 대부분 마지막 값을 취하고, 일부는 첫 번째를 유지하며, 몇몇은 오류를 던지고, 드물게 둘 다 유지합니다. 명세상 어느 것도 틀리지 않습니다.
이것은 실제 보안 위험입니다. 한 서비스가 첫 번째 role을 읽어 {"role":"user","role":"admin"}을 검증하고 다른 서비스가 마지막을 따라 동작하면, 공격자는 검증을 지나쳐 권한을 몰래 넣을 수 있습니다. 결정이 중요한 곳에서는 중복 키를 오류로 취급하고, 어떤 값이 '이기는지'에 결코 의존하지 마세요.
숫자: 정수는 없고 골칫거리만 있다
JSON에는 숫자 타입이 하나뿐입니다. RFC는 정밀도도 범위도 정의하지 않고 — 상호운용을 위해 구현이 IEEE 754 배정밀도에 맞는 값을 다룰 것으로 기대된다고만 언급합니다. 실제로 이것은 2^53(즉 9007199254740992)을 넘는 무엇이든 정밀도를 잃을 수 있다는 뜻입니다.
고전적 버그: 9007199254740993 같은 64비트 ID는 JavaScript에서 JSON.parse 후 9007199254740992가 됩니다. 숫자가 double이기 때문입니다. 해법은 큰 정수를 문자열로 운반하는 것입니다. 사람들이 잊는 규칙 몇 가지 더: 선행 0 불가(007은 무효), 선행 + 불가, 끝의 소수점 불가(1.은 무효), 그리고 NaN과 Infinity는 유효한 JSON이 아닙니다 — null이나 문자열로 직렬화하세요.
문자열, 유니코드, 인코딩
문자열은 큰따옴표를 써야 하고, U+0000부터 U+001F까지의 제어 문자는 이스케이프해야 합니다. 슬래시는 선택적으로 \/로 이스케이프할 수 있는데, 그래서 일부 인코더는 <\/script>를 내보냅니다 — HTML script 태그를 깨지 않는 합법적 방법입니다. RFC 8259는 시스템 간 교환되는 JSON에 UTF-8을 의무화합니다.
기본 다국어 평면(BMP) 밖의 문자, 예를 들어 대부분의 이모지는 하나가 아니라 \u 이스케이프의 서로게이트 쌍으로 씁니다. 외톨이의, 짝 없는 서로게이트는 기술적으로 무효이지만 일부 파서는 받아들입니다 — 당신의 데이터가 더 엄격한 구현으로 넘어갈 때만 드러나는 이식성 함정입니다.
JSON처럼 보이지만 아닌 것들
가장 흔한 실수는 JSON을 JavaScript처럼 다루는 데서 옵니다. 끝 쉼표는 무효입니다. JSON에는 주석이 존재하지 않습니다 — JSON5와 JSONC가 그것을 더하지만, 엄격한 RFC 8259 파서는 거부합니다. 작은따옴표 문자열과 따옴표 없는 객체 키도, 아무리 익숙해 보여도 무효입니다.
더 미묘한 것은 바이트 순서 표시(BOM)입니다. RFC 8259는 생산자가 BOM을 추가해서는 안 되고(MUST NOT) 소비자가 하나를 무시해도 된다(MAY)고 말하지만, BOM은 JSON의 일부가 아니므로 파일 첫머리의 UTF-8 BOM은 엄격한 파서를 바로 첫 바이트에서 실패하게 할 수 있습니다. 의미 없는 공백은 공백, 탭, 줄바꿈(LF), 캐리지 리턴(CR)으로 제한됩니다 — 그 외에는 없습니다.
요점
보안이나 정확성 결정이 값에 달린 곳에서는 중복 키를 거부하세요.
64비트 정수는 문자열로 운반하세요 — JavaScript 같은 배정밀도 파서에서 살아남도록.
객체 키 순서에 의존하지 마세요 — 대부분의 파서가 삽입 순서를 보존하더라도 명세는 객체를 순서 없는 것으로 정의합니다.
RFC 8259 아래에서는 어떤 값이든 유효한 최상위 JSON임을 기억하세요 — 객체와 배열만이 아닙니다.
주석이나 끝 쉼표를 원하면 당신이 원하는 것은 JSON5/JSONC입니다 — 엄격한 파서가 데이터를 보기 전에 변환하세요. 이 사이트의 JSON 포매터는 엄격한 RFC 8259 문법에 대해 검증하므로 이런 문제가 즉시 드러납니다.