JSON ke CSV: dari mana header berasal dan mengapa nilai berubah
Intinya: Sembilan eksperimen pada konverter nyata: strategi "kunci dari baris pertama" kehilangan kolom secara diam-diam, null, ketiadaan, dan string kosong semuanya menjadi sel kosong, bilangan bulat 20 digit kehilangan presisi sejak JSON.parse, dan titik dalam kunci membuat dua jalur berbeda bertabrakan di kolom yang sama. Bagaimana header dibangun, nilai mana yang terdistorsi, dan bagaimana memilih strategi sesuai penggunaan di hilir.

Ketika konversi JSON ke CSV kehilangan data, hampir tidak pernah itu terjadi pada langkah "konversi". Kehilangan terjadi dalam dua pertanyaan yang tak pernah ditanyakan: siapa yang memutuskan header, dan apakah setiap sel masih dikenali sebagai nilai yang dulu ia miliki?
Satu kolom menghilang dalam tiga puluh detik
Banyak konverter — termasuk pipeline jq yang idiomatis — mengambil header hanya dari rekaman pertama. Reproduksi akibatnya di Python:
data = [{"id": 1, "name": "A"}, {"id": 2, "email": "b@x.com"}]
first_keys = list(data[0].keys()) # ['id', 'name']
rows = [[r.get(k, "") for k in first_keys] for r in data]
# Email rekaman kedua menghilang tanpa error maupun peringatan
Kunci email tidak ada di header: ia bahkan tidak berhak menjadi sel kosong — kolom tidak pernah dibuat, dan nilainya dibuang diam-diam. Koreksinya adalah gabungan kunci: menelusuri semua rekaman dan membuat kolom untuk setiap kunci yang muncul di mana pun. Data yang sama melalui gabungan menghasilkan header id,name/email, dengan sel kosong di tempat name absen — dan tidak ada yang hilang.
Konverter langsung di halaman /convert/csv kami menerapkan persis strategi ini. Dengan input [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}], ia menghasilkan:
id,name,email
1,A,
2,,b@x.com
Kunci diurutkan menurut kemunculan pertama, dan kolom yang absen menjadi sel kosong. jsonoperations dan jsonbeam sama-sama menyebut ini "perbaikan bug klasik" — tetapi "memakai gabungan" hanya separuh cerita. Gabungan punya harga sendiri; itulah bagian berikutnya.
Tiga cara membangun header — dan siapa yang cocok dengan masing-masing
| Strategi header | Cara dibangun | Kekuatan | Harga |
|---|---|---|---|
| Kunci baris pertama | Hanya rekaman pertama yang dibaca | Sedikit kolom, stabil, satu baris jq | Kolom baru dari rekaman berikutnya hilang diam-diam |
| Gabungan kunci | Semua rekaman dibaca | Tidak ada kolom yang hilang | Kolom renggang dan banyak sel kosong saat bentuk bervariasi |
| Daftar kolom eksplisit | Didefinisikan manual atau oleh skema | Sepenuhnya terkendali, dapat direproduksi | Butuh pemeliharaan; kolom baru menuntut pembaruan manual |
Kriteria pemilihan bukan "mana yang lebih canggih" melainkan apa yang penerima lakukan dengan CSV ini:
- Rekan yang melirik sekilas: kunci baris pertama cukup, bentuk rekaman biasanya seragam.
- Laporan atau pipeline data: gabungan kunci adalah syarat minimum — sel kosong itu sendiri adalah informasi.
- Ekspor otomatis berulang: daftar kolom eksplisit. Pergeseran bentuk (kolom baru dari hulu, nama kunci diganti) harus gagal dengan keras, bukan diam-diam menghasilkan tabel yang tak ada yang sadar salah.
Opsi ketiga adalah yang paling terabaikan. Dalam otomasi, "konversi berhasil" dan "konversi benar" adalah dua pernyataan berbeda — dan daftar kolom eksplisit adalah satu-satunya mekanisme yang memisahkan keduanya.
Ketika kunci mengandung titik, jalur pelataran bertabrakan
Pelataran dengan notasi titik mengubah {"a": {"b": 2}} menjadi kolom a.b. Tetapi bagaimana jika kunci itu sendiri mengandung titik? Diuji dengan pandas:
import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# Hanya satu kolom yang selamat: ['a.b'] — dua kunci yang sama sekali berbeda bertabrakan pada jalur yang sama
Kunci literal a.b dan jalur bersarang a → b menghasilkan nama kolom yang sama, satu menimpa yang lain, dan data rusak diam-diam. Contoh ini dibuat-buat, tetapi dengan data nyata juga terjadi: nama metrik pemantauan (cpu.usage) dan kolom log (error.type) menyukai titik.
Dua penawar:
- Ganti pemisah.
sep="_"atau pemisah kustom — setelah memastikan kunci asli tak pernah memuatnya. - Ganti notasi jalur. JSON Pointer memakai
/sebagai pemisah (/a/b); konverter Excel toolvore menempuh jalan ini. Ia punya kelas ambiguitas yang sama — kunci dengan/ditulis~1— tetapi aturan escape ternormalisasi di RFC 6901, bukan ditemukan sendiri oleh setiap alat.
Apa pun pilihan Anda, tulis aturannya di dokumentasi ekspor. Header CSV adalah satu-satunya "skema" yang dibawa data ini; pembaca tidak akan menemukan aturan pelataran Anda di tempat lain.
Array: tiga strategi, masing-masing kehilangan hal berbeda
Array adalah hambatan sesungguhnya di jalur JSON ke CSV: satu sel tak bisa memuat banyak nilai. Tiga strategi, dengan kerugian yang terverifikasi di lapangan:
| Strategi | Bentuk keluaran | Yang hilang | Ideal untuk |
|---|---|---|---|
| JSON dalam sel | Array diserialisasi sebagai string JSON dalam sel | Alat tabel tak bisa memfilter maupun mengagregasi elemen | Kelestarian dulu, inspeksi manual |
| Kolom terindeks | tags.0, tags.1, satu kolom masing-masing |
Jumlah kolom meledak dengan array terpanjang; banyak sel kosong | Array kecil berpanjang tetap |
| Baris yang diledakkan | Satu baris per elemen, kolom induk diduplikasi | Pembengkakan baris; perilaku array kosong harus ditentukan terpisah | Analisis rinci, agregasi per elemen |
Konverter /convert/csv dan /convert/table kami memakai JSON dalam sel. Input [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}] menghasilkan:
orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"
Butuh baris yang diledakkan? pandas record_path melakukannya langsung:
orders = [{"orderId": "A001", "total": 99,
"items": [{"sku": "X1", "qty": 2}, {"sku": "Y9", "qty": 1}]}]
items = pd.json_normalize(orders, record_path=["items"],
meta=["orderId", "total"], sep=".")
# Dua baris rincian; orderId dan total diduplikasi di masing-masing
Dalam peledakan ada tiga jebakan yang perlu diputuskan di muka: meledakkan beberapa array sekaligus membangun produk Kartesius; array kosong secara bawaan tidak menghasilkan baris apa pun (daftar pelanggan Anda kehilangan orang); dan kolom induk yang diduplikasi menuntut deduplikasi saat agregasi per orderId di hilir. Panduan convertcsvonline menempatkan "tentukan dulu granularitasnya" sebagai langkah pertama persis karena itu — begitu granularitas terkunci, ketiga pertanyaan masing-masing punya satu jawaban.
Teruji: nilai yang keluar dari konversi dalam keadaan berubah
Uji kelestarian yang dijalankan pada konverter nyata kami (Node v22.13.1 + papaparse, implementasi yang sama seperti /convert/csv):
1. null, ketiadaan, dan string kosong semuanya menjadi sel kosong. Input [{"a":null},{"b":""},{"c":1}] — ketiga sel keluar kosong. CSV tidak punya tiga keadaan, hanya "ada karakter" dan "tak ada karakter". Setelah satu putaran pulang, ketiganya terbaca ""; semantik aslinya tak tertolong.
2. Objek dan array bersarang menjadi string JSON. Kutip digandakan sesuai RFC 4180 ({""name"":""张三""}), dan begitu sel ditempelkan ke parser JSON mana pun nilai kembali — karena itulah JSON dalam sel menjaga kelestarian.
3. Bilangan bulat 20 digit kehilangan presisi sejak saat parsing — dan kesalahannya bukan pada CSV. Input "id": 9007199254740993; di CSV sudah berbunyi 9007199254740992. JSON.parse di JavaScript memakai double IEEE 754: kehilangan terjadi saat parsing, sebelum konverter CSV mana pun. Python json.load memeliharanya utuh. Kolom berjenis identitas (ID snowflake, nomor pesanan) sebaiknya datang sebagai string sejak dari produsen. Lebih lanjut soal presisi angka di mengapa angka besar kehilangan presisi.
4. Nol di depan pada string aman di lapisan teks CSV. "phone": "00123" tetap 00123 di CSV, karena ia string dari awal hingga akhir dan tak ada yang mencoretnya. Yang mencoret nol adalah langkah berikutnya, saat Excel memperlakukan sel sebagai angka — lihat empat cara mengonversi JSON ke Excel.
5. Kolom yang memuat koma, kutip, atau baris baru. papaparse menaati RFC 4180: kutip kolomnya, gandakan kutip bagian dalam. "He said \"hi\", ok" keluar sebagai "He said ""hi"", ok" dan melewati putaran pulang tanpa luka. Serializer tulisan tangan yang memotong pustaka hampir selalu gagal tepat di situ.
Putaran pulang: apa yang diubah sakelar inferensi
CSV → JSON harus menebak tipe untuk setiap sel. Konverter kami secara bawaan mematikan inferensi tipe dan mengembalikan semuanya sebagai string:
[{ "id": "9007199254740992", "phone": "00123" }]
Dengan typeInference aktif, id menjadi angka (presisinya sudah hilang) sementara phone tetap string — aturan inferensi mengecualikan bentuk dengan nol di depan seperti 010, yang bukan literal angka yang sah, sehingga string selamat karena untung.
Ingat asimetri ini: inferensi mati paling buruk hanya merampas tipe; inferensi hidup bisa merampas data. Kolom yang butuh angka dikonversi dengan definisi kolom eksplisit, bukan dipercakan pada inferensi global. Implementasi rowsToRecords dalam artikel kami sebelumnya JSON, CSV, dan data tabular persis prinsip itu dalam bentuk kode.
Memilih strategi menurut penerima: tabel keputusan
| Siapa menerima CSV | Strategi header | Perlakuan array | Tipe |
|---|---|---|---|
| Rekan yang membukanya sekali | Gabungan kunci | JSON dalam sel | Tanpa inferensi |
| Excel / pelaporan | Gabungan kunci | JSON dalam sel, atau kolom terindeks bila panjang tetap | Kolom identitas dipaksa jadi teks |
| Impor ke basis data | Daftar kolom eksplisit | Tabel master/detail terpisah, atau peledakan | Definisi kolom eksplisit |
| BI / agregasi | Daftar kolom eksplisit | Baris diledakkan, granularitas = unit analisis terkecil | Definisi kolom eksplisit |
| Pipeline terotomasi | Daftar kolom eksplisit | JSON dalam sel, peledakan di hilir | Inferensi mati, konversi kolom demi kolom |
Hukum bersama: semakin dekat ke manusia, semakin longgar boleh; semakin dekat ke mesin, semakin banyak yang harus eksplisit.
Daftar periksa untuk disalin
- [ ] Header berasal dari gabungan kunci lengkap atau daftar eksplisit? Jika tidak, mampu membuktikan tak ada rekaman yang kehilangan kolom?
- [ ] Bisakah kunci memuat titik atau garis miring? Tabrakan dengan pemisah pelataran sudah diperiksa?
- [ ] Strategi array sudah terdokumentasi? Perilaku array kosong sudah diputuskan?
- [ ] Untuk
null, ketiadaan, dan string kosong: apa yang dibaca penerima dari masing-masing? Pilih satu aturan dan bekukan. - [ ] Kolom berjenis identitas (ID, nomor pesanan, telepon) menempuh perjalanan sebagai string dari hulu ke hilir?
- [ ] Setelah peledakan, jumlah baris sama dengan jumlah panjang array? Diperiksa secara sampel?
- [ ] Kolom contoh dengan koma, kutip, dan baris baru selamat utuh melewati putaran pulang?
- [ ] Inferensi tipe global dimatikan saat impor ulang, dengan definisi eksplisit untuk kolom numerik?
Hanya ketika kedelapan punya jawaban tegas, CSV ini "benar" — bukan sekadar "berhasil dikonversi".
FAQ
Mengapa konverter tidak melatakan objek bersarang menjadi kolom secara bawaan?
Pelataran membuat jumlah kolom lepas kendali: bersarang dalam menghasilkan nama seperti user.address.geo.lat, dan dipadukan dengan gabungan kunci, satu rekaman berbentuk lain menambahkan setumpuk kolom. String JSON dalam sel menjaga kelestarian dan jumlah kolom yang stabil; latakan secara eksplisit saat dibutuhkan.
Bagaimana jika header memuat nama kolom duplikat?
Tabrakan dua kunci berbeda setelah pelataran (seperti a.b melawan a→b bersarang) harus diselesaikan di hulu: ganti pemisah atau beralih ke JSON Pointer. Untuk duplikat dalam satu objek yang sama, konverter biasanya menyimpan nilai terakhir; duplikat pada header bergantung implementasi — unparse papaparse tidak mendeduplikasi, dan Excel saat membuka diam-diam menggabungkan atau membuang duplikat. Karena itulah ia masuk daftar periksa.
Apakah kolom induk yang diduplikasi setelah peledakan adalah bug?
Bukan — itu desain. Baris yang diledakkan adalah baris rincian, dan baris rincian harus bisa tersambung kembali ke rekaman induk lewat kunci. Agregasikan di hilir menurut kunci induk; jika ukuran mengkhawatirkan, ekspor berkas rincian dan satu tabel master terpisah.
Bagaimana memastikan konversi saya tidak kehilangan satu kolom pun?
Lakukan audit kunci: hitung gabungan semua kunci pada JSON sumber, lalu himpunan header kolom CSV, dan ambil selisihnya. Selisih tak kosong berarti kolom yang hilang. Pada pipeline batch, tuliskan ini sebagai assert di pengujian: perubahan konverter apa pun yang mulai kehilangan kolom akan gagal otomatis.