JSON, YAML, CSV, XML 사이를 변환할 때 사라지는 것들

JSON, YAML, CSV, XML, TOML이 주석과 타입과 중첩과 날짜와 키 순서에서 어떻게 다른지, 그리고 이들 사이의 변환이 어떤 세부 사항을 조용히 버리는지 설명합니다.

데이터 형식 사이의 모든 변환은 원본이 표현할 수 있었지만 목적지가 표현할 수 없는 것을 버립니다. 어떤 손실은 명백합니다. YAML이 JSON이 되는 순간 주석은 사라집니다. 그러나 대부분은 조용합니다. 날짜가 문자열로 도착하고, 배열이 다섯 개의 열이 되며, 3으로 끝나던 ID가 이제 2로 끝납니다. 유용한 습관은 어떤 세부 사항이 사라질지를 사라지기 전에 아는 것입니다.

각 형식이 담을 수 있는 것

주석타입중첩날짜키 순서중복 키
JSON없음string, number, boolean, null, object, array가능고유 타입 없음명세상 순서 없음, 대부분의 파서는 유지정의되지 않음, 보통 마지막 값이 남음
YAML있음, #JSON의 타입에 더해 1.1에서는 타임스탬프 등가능1.1에서는 지원, 1.2에서는 태그에 따라 다름파일 안에서는 유지, 파싱 후에는 아님규격상 무효이지만 흔히 허용됨
CSV없음없음, 모든 셀이 텍스트불가능, 평평한 행만없음머리글 행이 고정적법하며 모호함
XML있음, <!-- -->스키마 없이는 없음, XSD로 타입 지정가능스키마를 통해서만요소는 순서 있음, 속성은 없음요소 반복은 정상, 속성 중복은 오류
TOML있음, #string, integer, float, boolean, date, time, date-time, array, table가능, 테이블을 통해지원, 일급 타입의미 없음언제나 오류

놀랄 일의 대부분은 두 열에서 나옵니다. 타입007이 살아남을지 아니면 숫자 7로 도착할지를 결정하고, 중첩은 새로운 관례를 만들어내지 않고도 CSV로 변환하는 것이 가능한지를 결정합니다.

CSV는 평평하고 모든 셀은 텍스트입니다

CSV의 형태는 하나뿐입니다. 필드로 이루어진 행들입니다. 셀 안에 객체를 표현할 방법이 없으므로 변환기는 무언가를 만들어내야 합니다. 보통은 점으로 이어진 경로로 평탄화하며, 그래서 다음 데이터는

[{ "id": 1, "name": { "first": "Ada" }, "tags": ["admin", "ops"] }]

이렇게 됩니다.

id,name.first,tags.0,tags.1
1,Ada,admin,ops

두 가지 일이 생기기 전까지는 이 구조가 깔끔하게 복원됩니다. 길이가 서로 다른 배열은 머리글을 가장 긴 레코드에 맞춰 넓히므로, 태그가 세 개인 행 하나가 tags.2 열을 추가하고 나머지 모든 행에는 빈 셀을 남깁니다. 그리고 이미 점이 들어 있는 키는 그 뒤로 중첩 경로와 구별할 수 없게 되므로, 되돌아오는 길에 엉뚱한 구조가 재구성됩니다. 다른 방법은 셀 안에 JSON 문자열을 넣는 것입니다. CSV 규칙대로 따옴표를 겹친 "[""admin"",""ops""]" 같은 형태인데, 잃는 것은 없지만 뒤쪽의 어떤 것도 이를 이해하지 못합니다.

타입이 나머지 절반입니다. 셀은 문자를 담고 있으므로 읽는 쪽이 추측해야 합니다. 추측하면 007은 7이 되고, 1.01은 같은 값이 되며, 우편번호는 앞의 0을 잃고, TRUE라는 회사 코드는 불리언이 됩니다. 추측하지 않으면 모든 숫자가 문자열이 됩니다. CSV를 JSON으로 바꾸거나 TSV를 JSON으로 바꿀 때의 타입 추론은 복원이 아니라 합리적인 기본값을 가진 추측입니다. 원래 타입은 애초에 기록된 적이 없기 때문입니다.

빈 셀은 빈 문자열일 수도, null일 수도, 0일 수도, 해당 없음일 수도 있습니다. 그 의미를 정하고 명시하십시오. 파일은 그것을 말해줄 수 없습니다. CSV를 TSV로 바꾸는 것처럼 구분자만 바꾸는 일은 여기에서 유일하게 안전한 변환입니다. 망가뜨릴 타입 자체가 없기 때문입니다.

YAML은 타입을 대신 추측합니다

YAML은 따옴표 없는 텍스트에서 타입을 추론하는데, 이 점이 YAML을 쓰기에는 편하게 만들고 변환하기에는 위험하게 만듭니다.

country: no
version: 1.20
build: 010
start: 12:30

PyYAML을 비롯한 여러 구형 라이브러리가 아직 구현하고 있는 YAML 1.1 파서에서 이것은 다음과 같습니다.

{ "country": false, "version": 1.2, "build": 8, "start": 750 }

네 값 모두 의미가 바뀌었습니다. no는 이른바 노르웨이 문제입니다. YAML 1.1은 y, yes, no, on, off를 불리언으로 취급하므로 국가 코드가 false가 됩니다. 1.20은 부동소수점이므로 끝의 0이 사라졌습니다. 010은 8진수 패턴에 맞아 8이 되었습니다. 12:30은 60진수 패턴에 맞아 분 단위 값인 750이 되었습니다.

현행 js-yaml 같은 YAML 1.2 코어 스키마 파서는 같은 파일을 읽어 "no"라는 문자열, 01010, "12:30"은 문자열로 만듭니다. 파일 하나, 파서 둘, 서로 다른 데이터입니다. 명세끼리 실제로 의견이 다릅니다. 1.2는 60진수 규칙과 불리언 단어 규칙을 의도적으로 없앴습니다.

방어 수단은 문자 하나입니다. 텍스트여야 할 것은 모두 따옴표로 감싸십시오. country: "no", version: "1.20"처럼 씁니다. YAML과 JSON 변환기는 파서가 그 파일을 어떻게 해석했는지 보여줍니다. JSON에는 그 모호함을 숨길 자리가 없기 때문입니다.

JSON에는 날짜도, 주석도 없고 53비트 천장이 있습니다

JSON의 숫자는 IEEE 754 배정밀도 값이므로 정수가 정확하게 유지되는 범위는 2^53, 즉 9007199254740992까지입니다. 그 너머는 이렇습니다.

{ "id": 9007199254740993 }

대부분의 언어에서 이 값을 파싱했다가 다시 직렬화하면 9007199254740992로 돌아옵니다. 그 값은 아예 표현될 수 없으므로 반올림되었다기보다 존재할 수 없는 것에 가깝습니다. Discord와 Twitter를 비롯한 스노플레이크 방식 API들이 ID를 문자열로 보내는 이유가 이것입니다. 64비트 정수는 JSON 숫자에 들어가지 않습니다.

날짜 타입도 없으므로 날짜는 문자열로 이동하며 관례상 ISO 8601을 씁니다. 그리고 "2026-08-25"가 텍스트가 아니라 날짜임을 표시해 주는 것은 아무것도 없습니다. TOML이나 YAML 1.1 파서를 거치면 진짜 날짜가 되었다가 다른 서식으로 되돌아올 수도 있습니다. JSON에는 주석도 없고 끝의 쉼표도 허용되지 않으므로, YAML이나 TOML 원본에 달아둔 설명은 옮겨지는 것이 아니라 사라집니다. 샘플에서 TypeScript 인터페이스를 생성하는 작업은 이 두 가지 맹점을 그대로 물려받습니다. 문자열은 날짜가 아니고, 없는 필드는 선택적 필드가 아닙니다.

XML은 어느 방향으로도 JSON 안에 들어맞지 않습니다

XML은 속성과 자식 요소를 구별하지만 JSON에는 키밖에 없습니다.

<user id="7" active="true">
  <name>Ada</name>
  <tag>admin</tag>
  <tag>ops</tag>
</user>

변환기는 하나의 관례를 고릅니다. 보통 속성 앞에 @를 붙이고 반복되는 요소를 배열로 묶습니다. 이는 특정한 방식으로 손실을 일으킵니다. <tag>가 하나뿐이면 배열이 만들어지지 않으므로, 출력 구조가 스키마가 아니라 데이터에 따라 달라지고, 목록을 기대하는 코드는 태그가 하나뿐인 레코드에서 깨집니다. <p>Hello <b>there</b> friend</p> 같은 혼합 콘텐츠에는 대응하는 JSON 표현이 없고, 네임스페이스와 CDATA와 주석도 사라집니다.

반대 방향에서는 다른 것을 잃습니다. 요소 이름은 숫자로 시작할 수 없고 공백을 담을 수 없으므로 임의의 JSON 키는 변형되어야 합니다. null은 관례상 빈 요소나 xsi:nil이 됩니다. 최상위 배열에는 원본에 없던 감싸는 요소가 필요합니다. 그리고 어떤 값이 원래 속성이었는지를 기록하는 것이 JSON에는 없으므로, JSON을 XML로 바꾸면 요소만 나옵니다. CSV를 XML로 바꾸는 도구는 그 선택을 드러내 놓고 요소로 할지 속성으로 할지 물어봅니다.

키 순서와 중복 키

키 순서는 어디에서도 보장되지 않지만 거의 모든 곳에서 유지됩니다. JSON 명세는 객체를 순서 없는 것으로 규정하지만, 주요 파서는 모두 삽입 순서를 유지합니다. 그래도 비교하거나 체크섬을 계산할 대상이라면 키를 정렬해 두는 편이 더 안전합니다. 키가 자리만 옮긴 두 문서의 diff는 읽을 수가 없기 때문입니다. JSON 비교 도구처럼 구조를 기준으로 비교하면 위치가 아니라 키로 대응시키므로 이 문제 자체가 사라집니다.

중복 키는 더 날카로운 문제입니다. JSON은 아무 말도 하지 않으므로 파서마다 다르고 대부분 조용히 마지막 값을 남깁니다. YAML은 중복을 금지하지만 많은 파서가 그래도 받아들입니다. TOML은 아예 거부합니다. CSV는 같은 머리글을 가진 열 두 개를 허용하고 판단은 읽는 쪽에 맡기는데, 낯선 파일을 먼저 CSV 검증기에 통과시켜야 할 이유로 충분합니다. 반복이 우연이 아니라 의미를 갖는 것은 XML뿐입니다.

의도를 갖고 변환하기

모든 파이프라인에는 손실이 일어나는 단계가 있습니다. 나중에 발견하지 말고 어느 단계인지 직접 고르십시오.

  • 가장 좁은 형식은 마지막에 거치십시오. XML을 JSON으로, 다시 CSV로 바꾸면 첫 단계에서 속성을 잃고 두 번째 단계에서 중첩을 잃습니다. CSV가 목적지라면 어떤 필드가 중요한지 정하고 의도적으로 평탄화하십시오.
  • 변환한 뒤가 아니라 변환하기 전에 따옴표를 붙이십시오. 버전 번호, 국가 코드, 우편번호, 앞자리가 0인 모든 값은 원본에서 따옴표 안에 있어야 합니다. no가 일단 false가 되고 나면 원래 텍스트는 복구할 수 없습니다.
  • 출력을 데이터로서 확인하십시오. 서식을 정리하고, 레코드 수를 확인하고, 껄끄러울 것으로 아는 행을 들여다보십시오. 이름 안의 쉼표, 가장 긴 배열, 가장 큰 ID 같은 것들입니다.
  • 왕복 변환의 차이를 확인하십시오. 변환해서 내보내고 다시 가져와서 비교하십시오. 비교 결과가 알려주는 것이 바로 그 변환이 실어 나르지 못하는 것입니다.
  • 원본을 보관하십시오. 변환된 파일은 파생물이며, 석 달 뒤 누군가 어떤 필드가 비어 있었는지 아예 없었는지 물어볼 때 답할 수 있는 것은 원본뿐입니다.