JSON yang sama, empat alat, empat hasil: cara memilih formatter
Intinya: Pemformatan bukan tanpa kehilangan. JSON yang sama dengan angka berawalan nol, BOM, atau bilangan bulat 20 digit menghasilkan hasil berbeda di Python, Node, jq, dan json.tool: ada yang memunculkan error, ada yang diam-diam mengubah nilai. Sepuluh sampel yang dijalankan sendiri pada empat jalur menghasilkan tabel pemilihan dan skrip validasi ketat.

Kesimpulan: perbedaan paling fatal antar alat format JSON bukan soal berapa spasi indentasi, melainkan input yang sama bisa memicu error di satu alat dan diam-diam mengubah data di alat lain. Sebelum memilih, jalankan satu sampel berisi nilai batas lewat alat itu — jauh lebih berguna daripada daftar fitur.
Orang mengira format itu cuma «menata ulang teks». Nyatanya setiap jalur menafsirkan ulang teks dengan parser miliknya sendiri. Saat parser longgar, error yang semestinya dilempar berubah jadi perubahan nilai tanpa pemberitahuan — sementara halaman tetap menampilkan tanda hijau «Berhasil diformat» yang menenangkan.
1. Gejala: satu input, empat jalur, empat hasil
Lingkungan uji: Python 3.11.1, Node v22.13.1, jq 1.7. Sepuluh string JSON pendek yang disusun manual dimasukkan lewat stdin, tanpa ditulis ke disk, tanpa mengubah konfigurasi. Empat jalur: json.loads, JSON.parse, jq -c ., python -m json.tool. ERR berarti melempar exception lalu berhenti, SILENT berarti tanpa error tetapi keluaran sudah beda dari masukan.
| Sampel | python json.loads | node JSON.parse | jq -c . | python -m json.tool |
|---|---|---|---|---|
S1 {"a":1,"b":[1,2]} |
OK | OK | OK | OK |
S2 {"a":1,} |
ERR | ERR | ERR | ERR |
S3 {'a':1} |
ERR | ERR | ERR | ERR |
S4 {"a":01} |
ERR char 6 | ERR position 6 | SILENT {"a":1} |
ERR |
S5 {"a":1 /*x*/} |
ERR char 7 | ERR | ERR | ERR |
S6 \ufeff{"a":1} |
ERR utf-8-sig | ERR position 0 | SILENT {"a":1} |
ERR |
S7 {"id":9007199254740993} |
OK exact | SILENT 9007199254740992 | OK exact | OK |
S8 {"a":1,"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
OK |
S9 {"a":1 |
ERR char 6 | ERR | ERR EOF | ERR |
S10 <html>502</html> |
ERR char 0 | ERR Unexpected token '<' | ERR | ERR |
Empat dari sepuluh sampel menghasilkan «tanpa error tapi data berubah»: S4, S6, S7, S8. Artinya kalau hanya percaya lampu hijau «Berhasil diformat», ada peluang empat puluh persen Anda sudah memegang data yang bukan lagi data asli. Yang menjebak: tak satu pun dari kasus ini masuk daftar error sintaks — file bisa dibuka, disalin, diekspor, dan kesalahannya ada di isi, bukan di sintaks.
2. Penyebab: lapisan sintaks dan lapisan semantik adalah dua hal berbeda
RFC 8259 bilang terus terang: bagian 1.2 menyarankan menghindari sejumlah praktik yang diizinkan ECMA-404 demi interoperabilitas maksimal; bagian 4 mewajibkan nama objek unik hanya sebagai SHOULD, tanpa menentukan bagaimana kunci duplikat ditangani; bagian 6 soal angka hanya memberi rekomendasi interoperabilitas. ECMA-404 melangkah lebih jauh, secara eksplisit menyatakan «agnostik terhadap semantik angka» — hanya mengatur representasi deretan angka, bukan penyimpanan di dalam bahasa. Dengan kata lain, spesifikasi menyerahkan area batas yang luas ke implementor.
Maka «format» terbelah jadi dua lapisan: lapisan sintaks memutuskan apakah teks legal, lapisan semantik memutuskan teks itu jadi nilai apa. Yang pertama punya jawaban baku, yang kedua tidak. jq membaca 01 sebagai 1 dan melewati BOM di awal adalah pilihan di lapisan semantik; Node memaksa bilangan bulat 20 digit ke double IEEE 754 adalah pilihan runtime; empat jalur sama-sama menyimpan nilai terakhir untuk {"a":1,"a":2} adalah kesepakatan umum parser besar. Tak ada yang melanggar «bisa di-parse», tapi semuanya mengubah data.
Cara memperbaiki error JSON parse bekerja di lapisan sintaks, menerjemahkan teks error jadi koordinat. Artikel ini mengurus langkah sebelumnya — kalau alat sama sekali tak memunculkan error, koordinat pun tak Anda dapat.
3. Empat perubahan data senyap, satu per satu
Ambil empat sampel senyap terpisah, perubahannya kecil sampai tak terlihat saat dibaca sekilas:
{"a": 01} // leading zero: jq emits {"a":1}
\ufeff{"a": 1} // BOM: jq skips the first three bytes
{"id": 9007199254740993} // 20-digit int: JSON.parse gives ...992
{"a": 1, "a": 2} // duplicate key: all four keep {"a":2}
Nol di depan dan BOM termasuk tipe «input memang sudah tak valid, tapi alat memakluminya». 01 ilegal di JSON standar (RFC 8259 bagian 6), BOM juga tak selayaknya ada di awal teks JSON; Python dan Node keduanya menolak tegas, jq memilih lanjut. Kelonggaran ini enak saat membersihkan log, tapi bencana saat pemeriksaan kontrak: upstream mengira sudah mengirim data salah yang perlu diperbaiki, sementara downstream sudah memegang hasil «yang kelihatannya normal».
Bilangan bulat besar termasuk tipe «input sepenuhnya legal, runtime tak mampu menampungnya». 9007199254740993 adalah angka JSON yang legal, tapi melewati 2^53-1; JSON.parse hanya bisa menyajikan 9007199254740992. int Python dan jq menyimpan nilai asli. Kalau ini nomor pesanan atau Snowflake ID, satu digit terakhir berubah berarti pencocokan pembukuan jadi tak cocok.
Kunci duplikat termasuk tipe «spesifikasi diam, implementasi memilih sendiri». Empat jalur semuanya senyap mengambil nilai terakhir, tanpa peringatan apa pun. RFC 8259 hanya bilang SHOULD, jadi ini bukan bug — tapi inilah kelas masalah paling sulit dilacak saat menggabungkan konfigurasi atau me-merge patch.
4. Kemampuan melokasi error: siapa kasih baris kolom, siapa cuma satu kalimat
Lihat enam sampel yang gagal, jumlah informasi dari empat jalur beda jauh. JSONDecodeError Python memberi msg, lineno, colno, dan pos sekaligus; Node v22 melaporkan position N (line 1 column M) plus potongan teks di titik error; python -m json.tool memakai informasi Python; jq hanya memberi satu kalimat parse error: ... at line N tanpa offset karakter.
Pada JSON minified satu baris, nomor baris dan kolom nyaris tak berguna — semuanya ada di baris 1. Yang dibutuhkan adalah offset karakter. Skrip berikut menyatukan pemeriksaan jadi «error + offset + konteks»:
import json, sys
def strict_check(text: str) -> None:
try:
json.loads(text)
print("OK")
except json.JSONDecodeError as e:
print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
print("...", text[lo:hi].replace("\n", "\\n"), "...")
sys.exit(1)
strict_check(open(sys.argv[1], encoding="utf-8").read())
Skrip ini lebih ketat dari jq . (menolak nol di depan dan BOM) dan memberi e.pos. Cara mengubah line 1 column 23611 jadi posisi byte, dan mengapa offset karakter dan offset byte di dokumen CJK bisa selisih tiga kali lipat, sudah dijelaskan tuntas di Debugging JSON yang Diminifikasi, tak diulang di sini.
5. Cara memperbaiki: pecah format jadi «validasi ketat» + «percantik»
Kesimpulannya sederhana: jangan biarkan satu tombol «Format» memikul validasi sekaligus percantikan. Pakai parser ketat untuk menentukan legalitas dulu, baru pilih alat untuk menata tampilan. Satu baris perintah sudah cukup memisahkan langkah pertama:
python3 -m json.tool input.json > /dev/null && echo "syntax OK" || echo "invalid syntax"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
Baris pertama hanya menjawab «bisa di-parse?», baris kedua baru memeriksa «strukturnya sesuai harapan?». Kalau ada bilangan bulat besar, baca dengan Python atau jq, jangan lewat JSON.parse; kalau sumber mungkin ber-BOM, sebutkan eksplisit encoding='utf-8-sig' saat membaca ketimbang berharap alat otomatis melewatkannya; kalau kunci mungkin duplikat, pindai tersendiri sebelum parse, karena tak satu pun dari empat jalur yang mengingatkan. Ditulis ke CI, inilah persis pipeline yang dibahas Editor, Command Line, dan Toolchain Otomasi, cuma ditambah satu lapis assertion kesetiaan data.
6. Cara memilih: tabel perbandingan empat jalur
| Jalur | Kekuatan | Risiko | Cocok untuk |
|---|---|---|---|
python json.loads / json.tool |
Ketat, kasih baris/kolom dan offset karakter, bilangan bulat tak kehilangan presisi | Menolak nol di depan dan BOM, encoding harus ditangani eksplisit | Validasi kontrak, CI, telusur log |
node JSON.parse |
Native di browser, error menyertakan potongan teks asli | Bilangan bulat besar dipotong senyap, BOM memicu error | Debug frontend, alat dalam browser |
jq -c . |
Ramah pipeline, hasil tercepat | Nol di depan dan BOM lolos senyap, tanpa offset karakter | Pipeline operasional, ambil field cepat |
Alat browser /parse, /editor |
Tampilan pohon, urut kunci, salin path, tanpa instalasi | Perilaku bergantung pada parser halaman, harus diuji sekali | Melihat manual, telusur error insidental |
Kuncinya bukan «mana yang paling banyak fitur», melainkan «apakah alat ini berbohong soal data saya?». Untuk data berisi nomor pesanan, nominal uang, atau ID perangkat, utamakan Python atau alat yang secara eksplisit menyatakan kesetiaan data; untuk sekadar melihat struktur tanpa disimpan, alat browser paling praktis. /parse di situs ini menjalankan validasi sintaks dan format di dalam browser — pas untuk «lihat dulu rupanya», tapi saat menyangkut bilangan bulat besar tetap sebaiknya dicek ulang dengan skrip di atas.
7. Privasi cuma salah satu batas
«Apakah data saya di-upload?» adalah pertanyaan paling sering untuk alat online, tapi ia terpisah dari «apakah data saya diubah?»: alat lokal yang bisa offline tetap saja bisa memotong bilangan bulat 20 digit; sebaliknya, alat online yang error-nya sangat ketat bisa saja meng-POST teks asli ke server. Dua hal ini harus diverifikasi terpisah, tak bisa saling menggantikan. FAQ privasi memberi tiga pemeriksaan dua menit; artikel ini melengkapi separuh lainnya — pakai daftar sampel di bawah untuk memastikan alat tak mengubah nilai secara senyap.
8. Daftar pemeriksaan mandiri yang bisa disalin
Simpan S1–S10 sebagai cases.txt, satu sampel per baris, tempel ke alat kandidat mana pun, dan cocokkan tiga hal: apakah sampel yang semestinya error benar-benar error, apakah error menyebut posisi konkret, apakah keluaran sampel yang lolos identik per karakter dengan masukan. Periksa khusus empat baris S4, S6, S7, S8 — cukup satu baris SILENT berarti jalur itu tak layak dipakai sendirian untuk validasi data.
Tambahkan tiga aturan rekayasa: masukkan validasi ketat ke pre-commit hook, jangan sampai ketidaksesuaian bilangan bulat baru ketahuan di produksi; tulis file sebagai UTF-8 tanpa BOM secara konsisten lintas bahasa; untuk JSON dari pihak ketiga simpan body respons mentah, dan hasil yang sudah diformat hanya untuk dibaca, bukan untuk ditulis balik.
Pertanyaan umum
Format berhasil, apakah berarti JSON sudah baik?
Tidak. S4, S6, S7, dan S8 semuanya «lolos» format, tapi nol di depan, BOM, bilangan bulat 20 digit, dan kunci duplikat ditimpa atau ditelan. Format hanya membuktikan teks diterima oleh suatu parser, bukan bahwa nilainya sama dengan yang Anda kirim; field kritis perlu diverifikasi nilai per nilai.
Mengapa jq menerima nol di depan dan BOM yang ditolak Python?
jq punya lexer sendiri, longgar terhadap angka seperti 01, dan juga melewati BOM di awal; Python dan Node mengikuti RFC 8259 secara ketat. Ini bukan bug versi, melainkan pilihan antara parser toleran dan parser ketat. Saat pembersihan yang pertama menghemat kerja, saat validasi kontrak yang kedua wajib.
Bagaimana mencegah bilangan bulat besar berubah jadi 9007199254740992?
Number di JavaScript adalah double IEEE 754 dengan batas aman 2^53-1. Membaca ID 20 digit lewat JSON.parse pasti memotongnya. Simpan ID semacam ini sebagai string, atau pakai int Python, jq, maupun pustaka parser yang mendukung BigInt; bila frontend benar-benar butuh angka, ganti field itu jadi string sebelum parsing.
Alat online atau command line lokal, mana yang dipakai?
Pisahkan berdasarkan sensitivitas dan tujuan: cuma melihat struktur atau mencari field, browser paling cepat; masuk pipeline dan butuh assertion kesetiaan data, pakai python -m json.tool plus skrip validasi ketat. Keduanya sebaiknya jalankan S1–S10 dulu, cocokkan error dan keluaran dengan harapan, baru putuskan kelas data apa yang dimasukkan.