JSONをCSVに変換する方法。エスケープ処理と落とし穴を正しく理解する
JSONをCSVに変換するとは、オブジェクトの配列を表形式のテキストに並び替える作業です。仕組み自体は単純ですが、値にカンマや引用符が含まれていたり、オブジェクトごとにキーの数が違っていたりすると、途端に扱いが厄介になります。特に引用符のエスケープルールを正しく理解していないと、変換結果を見て「なぜここに二重引用符が2つ並んでいるのか」と戸惑うことになります。
CSVは古い形式ですが、今も現役です。ExcelやGoogleスプレッドシートへのインポート、BIツールへのデータ取り込み、他システムとのバッチ連携など、JSONでは受け付けてもらえない場面で頻繁に必要とされます。APIから返ってきたJSONレスポンスをそのまま表計算ソフトに貼り付けたい、という需要は今でも尽きません。
具体例で見るエスケープ処理
次のJSON配列を例に、実際の変換結果を確認します。
[
{ "sku": "A100", "product": "Wireless Mouse, Black", "price": 19.99, "in_stock": true },
{ "sku": "A101", "product": "17\" Laptop Stand", "price": 42.5, "in_stock": false },
{ "sku": "A102", "product": "USB-C Cable", "price": 8.99, "in_stock": true }
]
これを変換すると、次のCSVになります。
sku,product,price,in_stock
A100,"Wireless Mouse, Black",19.99,true
A101,"17"" Laptop Stand",42.5,false
A102,USB-C Cable,8.99,true
ヘッダー行(sku,product,price,in_stock)は、3つのオブジェクトに登場したキーを最初に出現した順に並べたものです。ここでは全オブジェクトが同じキーを持っているため単純に見えますが、これは偶然ではなく、変換ロジックが最初のオブジェクトだけでなく配列全体を走査してキーの和集合を作っているためです。この点は後の章で詳しく扱います。
各行がなぜこの形になるのか、1行ずつ見ていきましょう。
1行目のWireless Mouse, Blackにはカンマが含まれています。CSVではカンマがそのまま列の区切り文字として扱われるため、値の中にカンマがあると区切り文字と区別がつかなくなってしまいます。これを避けるため、値全体を二重引用符で囲み、"Wireless Mouse, Black"という1つのセルであることを明示します。
2行目の17" Laptop Standには、リテラルな二重引用符が1文字含まれています(インチを表す記号です)。ここが最もつまずきやすいポイントです。RFC 4180のルールでは、値の中の二重引用符は2つ連続させた""に置き換え、さらに値全体をもう一組の二重引用符で囲みます。結果として"17"" Laptop Stand"という表記になります。一見引用符が過剰に見えますが、外側の一組が「この値は引用符で囲まれた1つのセルである」ことを示し、内側の""が「これはセルの終わりではなく、値の一部としての引用符である」ことを示しています。この値にはカンマは含まれていませんが、引用符を含んでいるという理由だけでも、囲み処理の対象になります。
3行目のUSB-C Cableにはカンマも引用符も改行も含まれていないため、何も囲まれず、そのまま出力されます。囲み処理が発生するのは「カンマ・二重引用符・改行のいずれかを含む値」だけで、それ以外の値は余計な引用符をつけずにそのまま通過します。
このツールはRFC 4180の規格に沿ってこの処理を自動で行うので、手動で引用符を数える必要はありません。
キーが揃っていないオブジェクトの扱い
現実のデータでは、配列内のすべてのオブジェクトが同じキーを持っているとは限りません。たとえば次のような入力を考えます。
[
{ "name": "Alice", "email": "[email protected]" },
{ "name": "Bob", "email": "[email protected]", "phone": "090-1234-5678" }
]
1件目にはphoneキーがなく、2件目にはあります。この場合、ヘッダー行は登場したすべてのキーの和集合、つまりname,email,phoneになります。ナイーブな変換ツールの中には、最初のオブジェクトのキーだけを見てヘッダーを決め、2件目以降で新しく登場したキーをそのまま切り捨ててしまうものがあります。それではBobの電話番号が変換結果から消えてしまいます。このツールは配列全体を先に走査してからヘッダーを確定するため、その問題は起きません。代わりに、phoneキーを持たないAliceの行では、該当するセルが単純に空欄になります。
name,email,phone
Alice,[email protected],
Bob,[email protected],090-1234-5678
エラーにはならず、値がないセルは空文字として出力される、という挙動を覚えておいてください。
ネストしたオブジェクトと配列は要注意
CSVはそもそも表形式、つまり平坦なデータのための形式です。JSONのようにオブジェクトの中にオブジェクトを入れる、という構造をそのまま表現する手段を持っていません。ここで2つの落とし穴が生まれます。
1つ目は、ネストしたオブジェクトの値です。次のような入力を考えます。
[
{ "name": "Alice", "address": { "city": "Tokyo", "zip": "100-0001" } }
]
addressの値はオブジェクトなので、そのままでは1つのセルに収まりません。変換処理は値を文字列化しようとしますが、JavaScriptのオブジェクトを素朴に文字列化すると[object Object]というリテラルな文字列になってしまいます。つまり住所の情報は消えるわけではなく、意味のない文字列に置き換わってしまうのです。これはデータが実質的に失われるのと同じ結果を招くため、変換前に気づかないと厄介です。
対処法は、変換する前にオブジェクトをフラット化しておくことです。address: { city, zip }を、トップレベルのaddress_cityとaddress_zipという2つのキーに分解しておけば、それぞれが独立した列として正しく出力されます。
[
{ "name": "Alice", "address_city": "Tokyo", "address_zip": "100-0001" }
]
2つ目は、配列値です。次の例を見てください。
[
{ "name": "Alice", "tags": ["admin", "verified"] }
]
tagsは文字列の配列ですが、変換結果はAlice,"admin,verified"のようになります。配列がadminとverifiedという2つの列に分かれるのではなく、カンマで連結された1つの文字列admin,verifiedになり、その文字列にカンマが含まれるためさらに引用符で囲まれる、という2段階の処理が起きています。角括弧[ ]はどこにも残らず、単なるカンマ区切りの1セルに潰れてしまいます。複数の列に分けたいのであれば、これもフラット化やタグの数だけ別々のキーを用意するといった前処理が必要です。
ツールを試す
下のウィジェットで、実際にJSONを貼り付けて結果を確認できます。入力は配列でなければならず、単一のオブジェクトを渡す場合も{ ... }ではなく[ { ... } ]のように角括弧で囲む必要があります。無効なJSONや配列以外の入力を渡すと、エラーにはなりますが画面が壊れることはありません。すべての処理はブラウザ内で完結し、入力したデータがサーバーに送信されることはありません。
よくある間違い
単一のオブジェクトをそのまま渡してしまう。 { "name": "Alice" }のように角括弧なしで渡すと、配列ではないためエラーになります。1件のデータでも[ { "name": "Alice" } ]のように配列で囲んでください。
ネストしたオブジェクトをそのまま渡し、[object Object]という文字列を目にする。 これは前述の通り、オブジェクトの文字列化によって起きる典型的なデータ損失です。変換前にaddress_cityのようなキー名でフラット化しておけば防げます。
配列値が複数列に分かれると思い込む。 ["red","blue"]のような配列は、red,blueという1つのカンマ区切り文字列に変換され、必要であれば引用符で囲まれます。角括弧は出力に残らず、別々の列にもなりません。列を分けたい場合は、変換前に自分でキーを分割してください。
一部のオブジェクトにしかないキーが消えると思い込む。 このツールは配列全体からキーの和集合を取ってヘッダーを作るため、後から登場したキーの列もきちんと作られます。値がないオブジェクトの該当セルは空欄になるだけで、列ごと消えることはありません。
よくある質問
JSONの配列でなくオブジェクト1件だけを変換できますか?
そのままでは変換できません。単一のオブジェクトも[ { ... } ]のように角括弧で囲み、要素数1の配列にしてから渡してください。
ネストしたJSONオブジェクトを変換するとどうなりますか?
ネストした値は[object Object]というリテラルな文字列に置き換わってしまい、元の情報は実質的に失われます。変換する前に、ネストしたキーをaddress_cityのようなトップレベルのキーにフラット化しておく必要があります。
配列を値に持つプロパティはどう変換されますか?
配列の各要素がカンマで連結された1つの文字列になります。たとえば["red","blue"]はred,blueという値になり、その文字列にカンマが含まれるため二重引用符で囲まれた1つのセルとして出力されます。複数の列には分かれません。
変換したデータはサーバーに送信されますか? いいえ。この変換はすべてブラウザ内のJavaScriptで完結しており、入力したJSONやCSVがどこかのサーバーに送信されることはありません。
値の中にカンマや引用符が含まれる場合、手動でエスケープする必要がありますか?
必要ありません。値にカンマ・二重引用符・改行のいずれかが含まれる場合、ツールが自動的に値全体を二重引用符で囲み、値の中の二重引用符を""に置き換えます。RFC 4180の標準的なエスケープルールに従っているため、手作業での調整は不要です。