Konversi XML ke JSON: Konvensi, Jebakan, dan Contoh Kode
Anda mengambil respons dari sebuah endpoint SOAP, sebuah feed RSS, atau sebuah sitemap.xml, dan ternyata isinya XML. Sementara tumpukan teknologi Anda berbasis JSON: JavaScript di sisi depan, REST di tengah, document store di lapisan bawah. Jadi Anda perlu mengonversi XML ke JSON, dan Anda meraih sebuah parser dengan harapan ini cukup satu baris.
Biasanya memang begitu, sampai keluarannya berbalik menggigit. Array yang Anda harapkan ternyata cuma satu objek tunggal. Atribut id raib. Kode pos seperti 01234 kembali jadi angka 1234. Tidak ada satu pun dari ini yang merupakan bug parser. Ini konsekuensi dari memetakan dua model data yang tidak sejajar, dan untuk mengonversi XML ke JSON dengan andal Anda perlu memahami konvensi yang menjembatani jurang itu.
Panduan ini membahas kenapa konvensi tersebut ada, empat cara melakukan konversi (browser, JavaScript, Python, CLI), aturan @_ dan #text yang dipakai bersama oleh pustaka-pustaka utama, lima jebakan yang diam-diam memakan data Anda, serta cara membalik JSON kembali ke XML untuk round-trip yang bersih. Contohnya bisa langsung dijalankan: tempel ke Node, Python, atau shell, dan keluarannya akan persis seperti yang tertera di komentar.
Mengapa XML-ke-JSON Membutuhkan Konvensi (Bukan Sekadar Format Ulang)
XML dan JSON tampak mirip di permukaan — keduanya pohon (tree) data bernama dan bersarang — tetapi model dasarnya berbeda dengan cara yang berdampak. Elemen XML bisa membawa atribut, menampung konten campuran (teks yang berselang-seling dengan elemen anak), dan hidup di bawah ruang nama (namespace). JSON tidak punya satu pun konsep itu; JSON cuma punya objek, array, dan empat tipe skalar. Mengubah yang satu menjadi yang lain bukan format ulang, melainkan penerjemahan antara dua tata bahasa yang salah satunya punya kata yang tak bisa dieja oleh yang lain.
Sebelum mengonversi apa pun, pastikan dulu sumbernya valid. Sebuah & liar yang tidak ter-escape atau tag yang tidak berpasangan langsung ditolak parser, jadi memeriksa kebenaran bentuk (well-formedness) lewat XML Formatter lebih dulu akan menghemat satu putaran kesalahan yang membingungkan.
Inilah titik di mana kedua model itu berpisah:
| Dimensi | XML | JSON |
|---|---|---|
| Tipe node | elemen, atribut, teks, konten campuran | objek, array, string, number, boolean, null |
| Batasan root | tepat satu elemen root diwajibkan | tidak ada batasan root |
| Atribut | ya (id="P01") | tidak ada (perlu konvensi @_) |
| Elemen berulang | saudara dengan nama sama itu sah | kunci objek tidak boleh berulang (perlu konvensi array) |
| Sistem tipe | teks tidak bertipe — semuanya string | tipe native |
| Namespace | ya (xmlns) | tidak ada |
Karena modelnya tidak cocok, setiap konversi XML-ke-JSON digerakkan oleh konvensi, bukan format ulang tanpa kehilangan data (lossless). Untungnya konvensi itu tidak sembarangan: fast-xml-parser (Node.js), xmltodict (Python), dan JAXB (Java) bertemu pada dua penanda yang sama — @_ untuk atribut, #text untuk teks konten campuran. Pelajari sekali, lalu pakai di runtime mana pun. Ketidakcocokan bentuk data semacam ini juga muncul pada konversi lain, misalnya soal inferensi tipe di panduan konversi CSV ke JSON.
Cara Mengonversi XML ke JSON: 4 Metode
Pilih metode yang sesuai konteks Anda: tempel sekali pakai yang cepat, sebuah layanan Node, sebuah pipeline Python, atau sebuah skrip shell di CI.
Metode 1 — Alat Berbasis Browser (Tanpa Setup, Privasi Diutamakan)
Untuk konversi sekali pakai, atau untuk XML yang sebaiknya tidak Anda tempelkan ke situs sembarangan, konverter di dalam browser adalah jalur tercepat. Tempelkan XML ke Konverter XML ke JSON, dan JSON-nya muncul seketika tanpa instalasi, tanpa akun, tanpa unggahan. Semuanya jalan di mesin JavaScript browser Anda, jadi data tidak pernah meninggalkan perangkat.
Detail terakhir itu lebih penting daripada kedengarannya. Amplop SOAP membawa token WS-Security, konfigurasi internal membawa connection string, ekspor membawa catatan pelanggan. Karena tidak ada apa pun yang dikirim, alat ini aman untuk XML yang mengandung kredensial atau muatan sensitif. Anda bisa membuktikannya sendiri: buka tab Network dan lihat nol permintaan terpicu saat Anda mengonversi.
Metode 2 — JavaScript / Node.js (fast-xml-parser)
Di Node, fast-xml-parser adalah pilihan standar. Tapi nilai bawaannya bisa bikin kaget — atribut diabaikan, nilai dikoersi (dipaksa tipe) — jadi opsi di bawah inilah yang sebenarnya Anda butuhkan untuk konversi yang setia:
// Konversi XML ke JSON di Node.js menggunakan fast-xml-parser
import { XMLParser } from 'fast-xml-parser';
const xml = `<catalog>
<product id="P01">
<name>Wireless Headphones</name>
<price currency="USD">79.99</price>
</product>
</catalog>`;
const parser = new XMLParser({
ignoreAttributes: false, // pertahankan atribut (bawaan membuangnya!)
attributeNamePrefix: '@_', // atribut menjadi kunci berawalan @_
textNodeName: '#text', // teks konten campuran masuk ke #text
parseAttributeValue: false, // tanpa koersi tipe pada atribut
parseTagValue: false, // tanpa koersi tipe pada teks elemen
});
const result = parser.parse(xml);
console.log(JSON.stringify(result, null, 2));
// {
// "catalog": {
// "product": {
// "@_id": "P01",
// "name": "Wireless Headphones",
// "price": {
// "@_currency": "USD",
// "#text": "79.99"
// }
// }
// }
// }
Dua pengaturan yang sering terlupa adalah ignoreAttributes: false dan parseTagValue: false. Yang pertama menjaga atribut id dan currency Anda; yang kedua mencegah parser mengubah "79.99" menjadi float dan "01234" menjadi 1234. Alasan kenapa mempertahankan string adalah bawaan yang aman akan kita bahas lagi di bagian jebakan.
Kalau Anda mau tanpa dependensi di browser, DOMParser native yang mem-parsing untuk Anda, dan Anda menelusuri DOM sendiri:
// XML ke JSON tanpa dependensi di browser menggunakan DOMParser
function xmlToJson(node) {
// Elemen hanya-teks → nilai string
const children = Array.from(node.children);
if (children.length === 0 && node.attributes.length === 0) {
return node.textContent.trim();
}
const obj = {};
// Atribut → awalan @_
for (const attr of node.attributes) {
obj['@_' + attr.name] = attr.value;
}
// Elemen dengan atribut DAN teks → #text
if (children.length === 0) {
obj['#text'] = node.textContent.trim();
return obj;
}
// Rekursi ke anak-anak, kumpulkan saudara bernama sama ke dalam array
for (const child of children) {
const value = xmlToJson(child);
if (obj[child.tagName] === undefined) {
obj[child.tagName] = value;
} else {
if (!Array.isArray(obj[child.tagName])) obj[child.tagName] = [obj[child.tagName]];
obj[child.tagName].push(value);
}
}
return obj;
}
const doc = new DOMParser().parseFromString(
'<catalog><product id="P01"><name>Wireless Headphones</name></product></catalog>',
'text/xml'
);
const json = { [doc.documentElement.tagName]: xmlToJson(doc.documentElement) };
console.log(JSON.stringify(json, null, 2));
// { "catalog": { "product": { "@_id": "P01", "name": "Wireless Headphones" } } }
DOMParser patuh pada XML 1.0, menangani CDATA dan referensi entitas, serta melaporkan kesalahan kebenaran bentuk, semuanya tanpa pasang paket. Konsekuensinya, logika penelusuran jadi milik Anda sendiri, termasuk aturan pengumpulan array yang ditunjukkan di atas.
Metode 3 — Python (xmltodict)
Di Python, xmltodict meringkas semuanya ke dalam pipeline pendek. Secara bawaan ia memakai @ sebagai awalan atribut dan #text untuk konten campuran:
# Konversi XML ke JSON di Python menggunakan xmltodict
import json
import xmltodict
xml = """<catalog>
<product id="P01">
<name>Wireless Headphones</name>
<price currency="USD">79.99</price>
</product>
</catalog>"""
data = xmltodict.parse(xml)
print(json.dumps(data, indent=2))
# {
# "catalog": {
# "product": {
# "@id": "P01",
# "name": "Wireless Headphones",
# "price": {
# "@currency": "USD",
# "#text": "79.99"
# }
# }
# }
# }
Secara bawaan xmltodict mempertahankan setiap nilai sebagai string, dan itu justru perilaku yang Anda mau. Satu opsi yang layak Anda tahu sejak awal adalah force_list, yang membereskan masalah array satu-versus-banyak sebelum sampai ke kode Anda:
# force_list menjamin <product> selalu berupa list, bahkan saat hanya ada satu
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product'] # sekarang selalu list
for p in products:
print(p['name'])
Tanpa force_list, satu <product> menghasilkan dict dan dua menghasilkan list, sehingga loop Anda crash di kasus item tunggal. Itu jebakan #1, yang kita bahas di bawah.
Metode 4 — CLI (yq / one-liner Python)
Untuk skrip shell dan pipeline CI, dua one-liner sudah mencakup sebagian besar kasus. yq buatan Mike Farah membaca XML dan langsung mengeluarkan JSON:
# Menggunakan yq (versi Go buatan Mike Farah)
yq -p=xml -o=json '.' input.xml
# Pipa dari stdin
cat sitemap.xml | yq -p=xml -o=json '.'
Jika xmltodict sudah ada di lingkungan Anda, one-liner Python ini tidak butuh biner tambahan:
python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml
Keduanya mengalir (stream) dari stdin, jadi pas masuk ke pipeline: berguna untuk mengonversi respons API di tengah skrip atau menormalkan sekumpulan berkas dalam satu langkah build.
Konvensi Atribut @_ dan #text Dijelaskan
Kebanyakan halaman konverter melewatkan bagian yang sebenarnya penting: apa arti kunci aneh @_ dan #text itu, dan kenapa keduanya ada. Pahami betul, dan keluarannya berhenti tampak sembarangan.
Atribut dipetakan ke kunci berawalan @_. Atribut tidak punya padanan JSON; tidak ada slot dalam objek untuk “metadata tentang objek ini” yang berbeda dari anak. Konvensinya adalah memberi atribut sebuah kunci berawalan @_:
<user id="42" role="admin"/>
→ { "user": { "@_id": "42", "@_role": "admin" } }
Kenapa harus @_? Karena tidak ada nama elemen XML yang valid boleh diawali @, awalan itu tidak akan pernah bentrok dengan kunci elemen-anak asli. Ini ruang nama cadangan yang tersembunyi di depan mata. (xmltodict memakai @ polos; fast-xml-parser memakai @_ secara bawaan. Prinsipnya sama.)
Konten campuran dipetakan ke #text. Ketika sebuah elemen punya atribut sekaligus nilai teks, teks itu butuh tempat untuk berdampingan dengan kunci-kunci atribut. Di situlah #text masuk:
<price currency="USD">29.99</price>
→ { "price": { "@_currency": "USD", "#text": "29.99" } }
Elemen teks-polos langsung menjadi nilai string. Tanpa atribut, tanpa anak, hanya teks, sehingga lapisan tidak langsung #text tidak diperlukan. <name>Alice</name> menjadi "name": "Alice". Kunci #text baru muncul ketika atribut memaksa nilai elemen menjadi objek.
Asimetri ini sumber bug yang halus. Nama elemen yang sama bisa menghasilkan string polos di satu dokumen dan objek @_/#text di dokumen lain, tergantung apakah instance itu membawa atribut. <price> tanpa atribut currency adalah string "29.99"; <price currency="USD"> yang sama adalah { "@_currency": "USD", "#text": "29.99" }. Kode yang membaca node.price langsung berfungsi untuk satu bentuk dan diam-diam rusak pada bentuk lain. Pengakses yang aman memeriksa tipenya dulu: const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;.
CDATA menjadi konten teks polos. Bagian <![CDATA[if (a < b) return;]]> cuma mekanisme escaping, jadi pembatasnya dilucuti dan teks di dalamnya dipertahankan: "if (a < b) return;". Tidak ada yang istimewa yang lolos ke JSON.
Setelah Anda punya keluaran, tempelkan ke JSON Formatter untuk memvalidasi JSON dan memastikan strukturnya cocok dengan yang diharapkan konsumen sebelum Anda merangkainya ke kode.
5 Jebakan XML-ke-JSON dan Cara Menghindarinya
Inilah kegagalan-kegagalan yang lolos dari tinjauan kode lalu muncul di produksi. Masing-masing berakar pada ketidakcocokan model yang dibahas di awal panduan ini.
1. Ambiguitas array (satu vs. banyak). Satu <item> menjadi objek; dua atau lebih menjadi array. Bentuk JSON-nya bergantung pada berapa banyak saudara yang kebetulan ada di dokumen itu. Kode konsumen seperti result.items.item.forEach(...) berfungsi saat pengujian, ketika fixture Anda punya tiga item, lalu melempar TypeError: not a function di produksi saat sebuah catatan cuma punya satu.
// Dua saudara <book> → array
// <library><book>A</book><book>B</book></library>
// → { "library": { "book": ["A", "B"] } }
// Satu <book> → objek, BUKAN array
// <library><book>A</book></library>
// → { "library": { "book": "A" } }
// Normalkan agar kedua kasus berperilaku identik
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // aman untuk 0, 1, atau banyak
Idiom [].concat(x ?? []) layak dihafal: nilai yang hilang menjadi [], objek tunggal menjadi [object], dan array yang sudah ada lewat tanpa berubah. Di Python, berikan force_list={'book'} ke xmltodict.parse() dan nilainya selalu list, jadi Anda melewati normalisasi sama sekali.
2. Atribut dibuang diam-diam. Beberapa pustaka secara bawaan mengabaikan atribut, dan fast-xml-parser persis begitu sampai Anda mengatur ignoreAttributes: false. Konversinya tampak berhasil, JSON-nya ter-parse dengan baik, lalu nilai id, currency, dan status Anda lenyap begitu saja. Atur selalu flag itu secara eksplisit ketimbang memercayai nilai bawaan.
3. Perataan namespace (flattening). Deklarasi xmlns menjadi kunci @_xmlns biasa, dan awalan dalam <soap:Body> bertahan cuma sebagai bagian dari kunci string "soap:Body". Semantiknya — bahwa dua awalan bisa terikat ke URI yang sama — hilang.
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>...</soap:Body>
</soap:Envelope>
→ {
"soap:Envelope": {
"@_xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
"soap:Body": "..."
}
}
Awalan soap: kini cuma teks dalam sebuah nama kunci; tidak ada yang tahu itu sebuah namespace. Kalau dua elemen dari namespace berbeda berbagi nama lokal yang sama, keduanya bisa bentrok. Saat penanganan namespace yang presisi memang dibutuhkan, simpan data dalam parser yang sadar-namespace dan jangan ratakan sama sekali ke JSON.
4. Tanpa koersi tipe — dan itu memang benar. <zip>01234</zip> tidak boleh menjadi 1234. Kode akun, kode pos, identifier ber-padding, dan desimal yang sensitif presisi sama-sama rusak di bawah koersi diam-diam. Konverter yang baik mempertahankan semuanya sebagai string dan membiarkan Anda mengoersi dengan sengaja:
// Jangan mengandalkan koersi implisit
if (config.timeout > 25) { /* rapuh: "30" > 25 kebetulan berhasil */ }
// Koersi secara eksplisit, hanya di tempat Anda tahu tipenya
if (parseInt(config.timeout, 10) > 25) { /* aman */ }
5. Berkurang (lossy): komentar, instruksi pemrosesan, dan urutan konten campuran. Komentar XML (<!-- ... -->) dan instruksi pemrosesan (<?xml-stylesheet ?>) tidak punya rumah di JSON, jadi dibuang. Urutan relatif teks yang berselang-seling dengan elemen anak bisa saja tidak bertahan saat round-trip. Kalau Anda perlu mempertahankan setiap byte — untuk memancarkan ulang dokumen sumber yang persis — jangan konversi sama sekali; pakai XML Formatter untuk memformat ulang atau memperkecil tanpa menyentuh model data.
Mengonversi JSON Kembali ke XML (Round-Trip)
Arah sebaliknya punya kerumitannya sendiri, karena JSON tidak punya aturan elemen-root sedangkan XML mewajibkan tepat satu. Konverter JSON ke XML pendamping menerapkan konvensi @_/#text yang sama secara terbalik, jadi perjalanan JSON → XML → JSON mempertahankan atribut, teks, dan struktur.
Bagian yang menarik di sini adalah normalisasi root. Konverter membereskan syarat root-tunggal lewat empat aturan:
- Objek berkunci-tunggal → kunci itu menjadi root:
{ "config": {...} }→<config>...</config>. - Objek berkunci-banyak → dibungkus dalam
<root>:{ "a": 1, "b": 2 }→<root><a>1</a><b>2</b></root>. - Array tingkat-atas → dibungkus sebagai
<root><item>...</item></root>, dengan<item>sebagai nama fallback tetap. - Nilai primitif →
<root>value</root>.
Selebihnya mencerminkan arah maju. Kunci @_ menjadi atribut, #text menjadi konten teks, dan array JSON di bawah sebuah kunci menghasilkan saudara-saudara bernama sama yang berulang — nama kunci dipakai ulang, tidak pernah dibuat tunggal (singularized):
// Konversi JSON ke XML di Node.js menggunakan fast-xml-parser
import { XMLBuilder } from 'fast-xml-parser';
const data = {
catalog: {
product: {
'@_id': 'P01',
name: 'Wireless Headphones',
price: { '@_currency': 'USD', '#text': '79.99' },
},
},
};
const builder = new XMLBuilder({
attributeNamePrefix: '@_', // kunci @_ menjadi atribut
textNodeName: '#text', // kunci #text menjadi konten teks
ignoreAttributes: false, // proses kunci @_
format: true, // cetak rapi
});
console.log(builder.build(data));
// <catalog>
// <product id="P01">
// <name>Wireless Headphones</name>
// <price currency="USD">79.99</price>
// </product>
// </catalog>
Satu detail yang diurus builder untuk Anda: karakter khusus dalam teks dan nilai atribut (<, >, &, ") di-escape menjadi referensi entitasnya, jadi keluarannya tetap berbentuk baik (well-formed).
FAQ
Bagaimana atribut XML dipetakan ke JSON?
Atribut menjadi kunci berawalan @_, jadi id="42" berubah menjadi "@_id": "42". Ini konvensi bersama dari fast-xml-parser dan xmltodict, dan awalan itu tidak pernah bentrok dengan nama elemen karena tidak ada nama elemen valid yang diawali @.
Mengapa XML ke JSON mempertahankan angka sebagai string?
Karena konverter tidak melakukan koersi tipe. Memaksa 01234 menjadi 1234 membuang nol di depan yang bermakna dari kode pos, nomor akun, dan ID ber-padding. Mempertahankan setiap nilai sebagai string adalah bawaan yang aman; koersi dengan sengaja di hilir, di tempat Anda tahu tipenya.
Apakah konversi XML ke JSON bersifat lossless?
Tidak. Komentar dan instruksi pemrosesan dibuang, semantik namespace cuma sebagian dipertahankan, dan urutan konten campuran bisa saja tidak bertahan saat round-trip. Saat Anda perlu mempertahankan setiap byte, pakai XML Formatter untuk memformat ulang XML ketimbang mengonversinya ke JSON.
Bagaimana elemen XML yang berulang ditangani di JSON?
Satu anak bernama sama menjadi objek; dua atau lebih menjadi array. Karena bentuknya bergantung pada jumlah saudara, kode konsumen Anda sebaiknya selalu menormalkan ke array agar kasus satu-item maupun banyak-item tertangani tanpa crash.
Apa yang terjadi pada namespace XML saat dikonversi ke JSON?
Deklarasi xmlns menjadi kunci @_xmlns biasa, dan awalannya tetap di dalam string nama elemen, seperti pada "soap:Body". Pengikatan semantik antara awalan dan URI tidak diinterpretasi, jadi namespace yang berbeda bisa rata menyatu.
Bagaimana cara mengonversi JSON kembali ke XML?
Pakai Konverter JSON ke XML pendamping. Ia menerapkan konvensi @_ dan #text yang sama secara terbalik, jadi atribut, konten teks, dan array dipetakan kembali secara simetris. Simetri itulah yang memungkinkan round-trip JSON → XML → JSON yang bersih.
Bisakah saya mengonversi XML dengan beberapa elemen root?
Tidak. Beberapa elemen tingkat-atas bukan XML yang berbentuk baik, jadi parser menolak inputnya. Bungkus dulu fragmen-fragmen itu dalam satu elemen root — ubah <a/><b/> menjadi <root><a/><b/></root> — baru konversi.
Kesimpulan
Konversi XML-ke-JSON digerakkan oleh konvensi, bukan format ulang. Aturannya konsisten di runtime mana pun: atribut dipetakan ke kunci @_, teks konten campuran ke #text, saudara berulang ke array, dan nilai tetap string sehingga nol di depan dan presisi bertahan. Jebakan yang perlu diingat adalah pergeseran bentuk satu-versus-array, atribut yang dibuang diam-diam, serta hilangnya komentar dan semantik namespace. Tidak satu pun yang merupakan bug; semuanya bisa diprediksi begitu Anda paham ketidakcocokan model di baliknya.
Saat Anda butuh konversi yang cepat dan privat, tempelkan ke Konverter XML ke JSON yang jalan sepenuhnya di browser. Validasi dulu sumbernya dengan XML Formatter, lalu balik ke arah sebaliknya lewat Konverter JSON ke XML saat Anda perlu XML round-trip. Untuk lebih jauh soal bagaimana model format data membentuk perilaku konversi, simak catatan tentang perbedaan YAML dan JSON.