Skip to content
Powrót do bloga
Poradniki

XML na JSON: konwencje, pułapki i przykłady kodu

Konwertuj XML na JSON poprawnie: jak mapowane są atrybuty, tablice i przestrzenie nazw, dlaczego wartości pozostają ciągami znaków, z kodem JavaScript, Python i przeglądarki.

13 min czytania

Konwersja XML na JSON: konwencje, pułapki i przykłady kodu

Pobierasz odpowiedź z punktu końcowego SOAP, kanału RSS albo pliku sitemap.xml — i okazuje się, że to XML. Twój stos technologiczny jest natywnie oparty na JSON: JavaScript na froncie, REST pośrodku, magazyn dokumentów na dole. Trzeba więc skonwertować XML na JSON, sięgasz po parser i spodziewasz się, że załatwi to jedna linijka.

Zwykle tak właśnie jest — dopóki wynik nie obróci się przeciwko tobie. Tablica, której oczekiwałeś, okazuje się pojedynczym obiektem. Atrybut id znika bez śladu. Kod pocztowy w rodzaju 01234 wraca jako liczba 1234. Żaden z tych przypadków nie jest błędem parsera. To skutek mapowania dwóch modeli danych, które do siebie nie pasują, a jedyny sposób, by niezawodnie konwertować XML na JSON, to zrozumieć konwencje, które wypełniają tę lukę.

Ten przewodnik wyjaśnia, dlaczego te konwencje istnieją, pokazuje cztery sposoby konwersji (przeglądarka, JavaScript, Python, CLI), reguły @_ i #text wspólne dla większych bibliotek, pięć pułapek prowadzących do cichej utraty danych oraz jak skonwertować JSON z powrotem na XML. Przykłady są gotowe do uruchomienia — wklej je do Node, Pythona albo powłoki, a wygenerują dokładnie taki wynik, jaki widać w komentarzach.

Dlaczego XML na JSON wymaga konwencji (a nie tylko zmiany formatu)

XML i JSON na pierwszy rzut oka wyglądają podobnie — oba są drzewami nazwanych, zagnieżdżonych danych — ale ich modele bazowe rozchodzą się w istotny sposób. Elementy XML mogą nieść atrybuty, zawierać treść mieszaną (tekst przeplatany z elementami potomnymi) i należeć do przestrzeni nazw. JSON nie zna żadnego z tych pojęć. Ma obiekty, tablice i cztery typy skalarne. Konwersja jednego na drugi to nie zmiana formatu, lecz tłumaczenie między dwiema gramatykami, z których jedna ma słowa, jakich druga nie potrafi przeliterować.

Zanim cokolwiek skonwertujesz, warto upewnić się, że źródło jest faktycznie poprawne. Pojedynczy nieucieczony znak & albo niedopasowany tag zostanie odrzucony przez parser, więc wcześniejsze przepuszczenie danych wejściowych przez Formatowanie XML w celu sprawdzenia poprawności składni oszczędza całą rundę mylących błędów.

Oto miejsca, w których oba modele się rozjeżdżają:

WymiarXMLJSON
Typy węzłówelementy, atrybuty, tekst, treść mieszanaobiekty, tablice, string, number, boolean, null
Ograniczenie korzeniawymagany dokładnie jeden element głównybrak ograniczenia korzenia
Atrybutytak (id="P01")brak (wymaga konwencji @_)
Powtarzane elementyrodzeństwo o tej samej nazwie jest dozwoloneklucze obiektu nie mogą się powtarzać (wymaga konwencji tablicowej)
System typówtekst jest nietypowany — wszystko jest ciągiem znakówtypy natywne
Przestrzenie nazwtak (xmlns)brak

Ponieważ modele do siebie nie pasują, każda konwersja XML na JSON jest sterowana konwencjami, a nie bezstratną zmianą formatu. Dobra wiadomość jest taka, że konwencje nie są arbitralne: fast-xml-parser (Node.js), xmltodict (Python) i JAXB (Java) zbiegły się do tych samych dwóch znaczników — @_ dla atrybutów, #text dla tekstu treści mieszanej. Naucz się ich raz, a przeniosą się między środowiskami uruchomieniowymi. Z tego samego powodu niedopasowania kształtu danych pojawiają się również w innych konwersjach, jak choćby pytania o wnioskowanie typów w Przewodniku po konwersji CSV i JSON.

Jak konwertować XML na JSON: 4 metody

Wybierz metodę pasującą do twojego kontekstu: szybkie jednorazowe wklejenie, usługa Node, potok Pythona albo skrypt powłoki w CI.

Metoda 1 — narzędzie w przeglądarce (zero konfiguracji, prywatność na pierwszym miejscu)

Do jednorazowej konwersji albo dla XML, którego wolisz nie wklejać na losową stronę, konwerter działający w przeglądarce to najszybsza droga. Wystarczy wkleić XML do Konwertera XML na JSON, a JSON pojawi się natychmiast — bez instalacji, bez konta, bez przesyłania na serwer. Wszystko działa w silniku JavaScript twojej przeglądarki, więc dane nigdy nie opuszczają maszyny.

Ten ostatni szczegół waży więcej, niż mogłoby się wydawać. Koperty SOAP przenoszą tokeny WS-Security, wewnętrzne konfiguracje zawierają parametry połączenia, a eksporty — rekordy klientów. Ponieważ nic nie jest przesyłane, narzędzie nadaje się do XML zawierającego poświadczenia lub wrażliwy payload. Można to sprawdzić samodzielnie: otwórz zakładkę Sieć i zobacz, że podczas konwersji nie pojawia się ani jedno żądanie.

Metoda 2 — JavaScript / Node.js (fast-xml-parser)

W Node fast-xml-parser jest standardowym wyborem. Ustawienia domyślne mogą jednak zaskoczyć — atrybuty są ignorowane, a wartości podlegają koercji — więc poniższe opcje to te, których faktycznie potrzebujesz do wiernej konwersji:

// Convert XML to JSON in Node.js using 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,    // keep attributes (default drops them!)
  attributeNamePrefix: '@_',  // attributes become @_-prefixed keys
  textNodeName: '#text',      // mixed-content text goes under #text
  parseAttributeValue: false, // no type coercion on attributes
  parseTagValue: false,       // no type coercion on element text
});

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"
//       }
//     }
//   }
// }

Dwa ustawienia, o których łatwo zapomnieć, to ignoreAttributes: false i parseTagValue: false. Pierwsze zachowuje atrybuty id i currency; drugie powstrzymuje parser przed zamianą "79.99" na liczbę zmiennoprzecinkową, a "01234" na 1234. Dlaczego zachowanie ciągów znaków to bezpieczne ustawienie domyślne, wyjaśnia sekcja o pułapkach.

Jeśli chcesz zero zależności w przeglądarce, natywny DOMParser wykona parsowanie za ciebie, a ty samodzielnie przejdziesz po drzewie DOM:

// Zero-dependency XML to JSON in the browser using DOMParser
function xmlToJson(node) {
  // Text-only element → string value
  const children = Array.from(node.children);
  if (children.length === 0 && node.attributes.length === 0) {
    return node.textContent.trim();
  }

  const obj = {};
  // Attributes → @_ prefix
  for (const attr of node.attributes) {
    obj['@_' + attr.name] = attr.value;
  }
  // Element with attributes AND text → #text
  if (children.length === 0) {
    obj['#text'] = node.textContent.trim();
    return obj;
  }
  // Recurse into children, collecting same-named siblings into arrays
  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 jest zgodny z XML 1.0, obsługuje sekcje CDATA i odwołania do encji oraz zgłasza błędy poprawności składni — wszystko bez instalowania pakietu. Kompromis polega na tym, że to ty odpowiadasz za logikę przechodzenia po drzewie, w tym za regułę zbierania tablic pokazaną powyżej.

Metoda 3 — Python (xmltodict)

W Pythonie xmltodict sprowadza całą robotę do krótkiego potoku. Domyślnie używa @ jako prefiksu atrybutów oraz #text dla treści mieszanej:

# Convert XML to JSON in Python using 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"
#       }
#     }
#   }
# }

Domyślnie xmltodict zachowuje każdą wartość jako ciąg znaków, czyli dokładnie to, czego potrzebujesz. Jedyna opcja, którą warto znać od początku, to force_list — rozwiązuje ona problem tablicy „jeden kontra wiele”, zanim dotrze on do twojego kodu:

# force_list guarantees <product> is always a list, even when there is one
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product']  # always a list now
for p in products:
    print(p['name'])

Bez force_list jeden <product> daje słownik, a dwa dają listę — i twoja pętla wykłada się na przypadku z jednym elementem. To pułapka nr 1, którą omawiamy poniżej.

Metoda 4 — CLI (yq / jednolinijkowiec w Pythonie)

Do skryptów powłoki i potoków CI dwa jednolinijkowce pokrywają większość przypadków. yq autorstwa Mike’a Faraha czyta XML i emituje JSON bezpośrednio:

# Using yq (Mike Farah's Go version)
yq -p=xml -o=json '.' input.xml

# Pipe from stdin
cat sitemap.xml | yq -p=xml -o=json '.'

Jeśli xmltodict jest już w twoim środowisku, jednolinijkowiec w Pythonie nie potrzebuje żadnego dodatkowego pliku binarnego:

python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml

Oba czytają strumieniowo ze standardowego wejścia, więc wpasowują się prosto w potok — przydatne do konwersji odpowiedzi API w trakcie skryptu albo normalizacji wsadu plików na etapie budowania.

Wyjaśnienie konwencji atrybutu @_ i #text

Większość stron z konwerterami pomija część najważniejszą: co oznaczają te dziwne klucze @_ i #text oraz dlaczego istnieją. Gdy raz to zrozumiesz, wynik przestaje wyglądać na przypadkowy.

Atrybuty mapują się na klucze z prefiksem @_. Atrybut nie ma odpowiednika w JSON — w obiekcie nie ma miejsca na „metadane o tym obiekcie”, które byłyby odrębne od elementu potomnego. Konwencja każe nadawać atrybutom klucz z prefiksem @_:

<user id="42" role="admin"/>
→ { "user": { "@_id": "42", "@_role": "admin" } }

Dlaczego akurat @_? Ponieważ żadna poprawna nazwa elementu XML nie może zaczynać się od @, prefiks nigdy nie zderzy się z kluczem prawdziwego elementu potomnego. To zarezerwowana przestrzeń nazw ukryta na widoku. (xmltodict używa samego @; fast-xml-parser domyślnie używa @_. Zasada jest identyczna.)

Treść mieszana mapuje się na #text. Gdy element ma jednocześnie atrybut i wartość tekstową, tekst musi mieć gdzie zamieszkać obok kluczy atrybutów. Tym miejscem jest #text:

<price currency="USD">29.99</price>
→ { "price": { "@_currency": "USD", "#text": "29.99" } }

Elementy zawierające czysty tekst stają się bezpośrednią wartością typu string. Brak atrybutów, brak elementów potomnych, tylko tekst — więc nie ma potrzeby pośrednictwa #text. <name>Alice</name> staje się "name": "Alice". Klucz #text pojawia się dopiero wtedy, gdy atrybuty wymuszają, by wartość elementu była obiektem.

Ta asymetria jest źródłem subtelnego błędu. Ta sama nazwa elementu może dać czysty ciąg znaków w jednym dokumencie i obiekt @_/#text w innym, zależnie od tego, czy dany konkretny egzemplarz niósł atrybut. <price> bez atrybutu currency to ciąg znaków "29.99"; ten sam <price currency="USD"> to { "@_currency": "USD", "#text": "29.99" }. Kod, który czyta node.price wprost, działa dla jednego kształtu i po cichu psuje się na drugim. Defensywny dostęp polega na sprawdzeniu typu: const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;.

CDATA staje się zwykłą treścią tekstową. Sekcja <![CDATA[if (a < b) return;]]> to tylko mechanizm ucieczki, więc ograniczniki są usuwane, a wewnętrzny tekst zachowany: "if (a < b) return;". Nic szczególnego nie przetrwa do JSON.

Gdy już masz wynik, wklej go do Formatowania JSON, aby zwalidować wyjściowy JSON i potwierdzić, że struktura odpowiada temu, czego oczekuje konsument, zanim wepniesz ją w kod.

5 pułapek konwersji XML na JSON i jak ich uniknąć

To są awarie, które przechodzą przez code review i wychodzą na jaw na produkcji. Każda z nich sprowadza się do niedopasowania modeli z początku tego przewodnika.

1. Niejednoznaczność tablicy (jeden kontra wiele). Pojedynczy <item> staje się obiektem; dwa lub więcej stają się tablicą. Kształt JSON zależy od tego, ile akurat rodzeństwa znalazło się w danym konkretnym dokumencie. Kod konsumenta w rodzaju result.items.item.forEach(...) działa w testach — gdzie twoja atrapa danych ma trzy elementy — a na produkcji rzuca TypeError: not a function, kiedy rekord ma dokładnie jeden.

// Two <book> siblings → array
// <library><book>A</book><book>B</book></library>
// → { "library": { "book": ["A", "B"] } }

// One <book> → object, NOT an array
// <library><book>A</book></library>
// → { "library": { "book": "A" } }

// Normalize so both cases behave identically
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // safe for 0, 1, or many

Idiom [].concat(x ?? []) warto zapamiętać: brakująca wartość staje się [], pojedynczy obiekt staje się [object], a istniejąca tablica przechodzi bez zmian. W Pythonie przekaż force_list={'book'} do xmltodict.parse(), a wartość zawsze będzie listą, więc całkowicie pominiesz normalizację.

2. Atrybuty po cichu pomijane. Kilka bibliotek domyślnie ignoruje atrybuty — fast-xml-parser robi dokładnie to, dopóki nie ustawisz ignoreAttributes: false. Konwersja wygląda, jakby się udała, JSON parsuje się bez zarzutu, a twoje wartości id, currency i status po prostu przepadły. Zawsze ustawiaj tę flagę jawnie, zamiast ufać ustawieniu domyślnemu.

3. Spłaszczanie przestrzeni nazw. Deklaracja xmlns staje się zwykłym kluczem @_xmlns, a prefiks w <soap:Body> przetrwa jedynie jako część klucza-ciągu "soap:Body". Semantyka — że dwa prefiksy mogą wiązać się z tym samym URI — zostaje utracona.

<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": "..."
    }
  }

Prefiks soap: jest teraz tylko tekstem w nazwie klucza; nic nie wie, że to przestrzeń nazw. Jeśli dwa elementy z różnych przestrzeni nazw współdzielą nazwę lokalną, mogą się zderzyć. Gdy precyzyjna obsługa przestrzeni nazw jest częścią wymagań, trzymaj dane w parserze świadomym przestrzeni nazw i w ogóle nie spłaszczaj ich do JSON.

4. Brak koercji typów — i tak właśnie ma być. <zip>01234</zip> nie może stać się 1234. Kody kont, kody pocztowe, dopełniane identyfikatory i wrażliwe na precyzję liczby dziesiętne — wszystkie te przypadki psują się przy cichej koercji. Dobry konwerter zachowuje wszystko jako ciąg znaków i pozwala ci konwertować typ świadomie:

// Don't rely on implicit coercion
if (config.timeout > 25) { /* fragile: "30" > 25 happens to work */ }

// Coerce explicitly, only where you know the type
if (parseInt(config.timeout, 10) > 25) { /* safe */ }

5. Stratność: komentarze, instrukcje przetwarzania i kolejność treści mieszanej. Komentarze XML (<!-- ... -->) i instrukcje przetwarzania (<?xml-stylesheet ?>) nie mają w JSON swojego miejsca i są odrzucane. Względna kolejność tekstu przeplatanego z elementami potomnymi może nie przetrwać obiegu w obie strony. Jeśli potrzebujesz zachować każdy bajt — by ponownie wyemitować dokładnie ten sam dokument źródłowy — w ogóle nie konwertuj; użyj Formatowania XML, aby przeformatować lub zminifikować bez naruszania modelu danych.

Konwersja JSON z powrotem na XML (obieg w obie strony)

Droga w przeciwnym kierunku ma swój własny haczyk, ponieważ JSON nie ma reguły elementu głównego, a XML wymaga dokładnie jednego. Towarzyszący Konwerter JSON na XML stosuje te same konwencje @_/#text w odwrotną stronę, więc podróż JSON → XML → JSON zachowuje atrybuty, tekst i strukturę.

Ciekawa część to normalizacja korzenia. Konwerter rozwiązuje wymóg pojedynczego korzenia za pomocą czterech reguł:

  • Obiekt z jednym kluczem → ten klucz staje się korzeniem: { "config": {...} }<config>...</config>.
  • Obiekt z wieloma kluczami → opakowany w <root>: { "a": 1, "b": 2 }<root><a>1</a><b>2</b></root>.
  • Tablica na najwyższym poziomie → opakowana jako <root><item>...</item></root>, z <item> jako stałą nazwą zastępczą.
  • Wartość prymitywna<root>value</root>.

Cała reszta odzwierciedla kierunek w przód. Klucze @_ stają się atrybutami, #text staje się treścią tekstową, a tablica JSON pod kluczem produkuje powtarzane rodzeństwo o tej samej nazwie — nazwa klucza jest ponownie używana, nigdy nie odmieniana na liczbę pojedynczą:

// Convert JSON to XML in Node.js using 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: '@_', // @_ keys become attributes
  textNodeName: '#text',     // #text key becomes text content
  ignoreAttributes: false,   // process @_ keys
  format: true,              // pretty-print
});

console.log(builder.build(data));
// <catalog>
//   <product id="P01">
//     <name>Wireless Headphones</name>
//     <price currency="USD">79.99</price>
//   </product>
// </catalog>

Jeden szczegół, który builder załatwia za ciebie: znaki specjalne w tekście i wartościach atrybutów (<, >, &, ") są ucieczane do odpowiadających im odwołań do encji, więc wynik pozostaje poprawny składniowo.

FAQ

Jak atrybuty XML mapują się na JSON?

Atrybuty stają się kluczami z prefiksem @_, więc id="42" zamienia się w "@_id": "42". To wspólna konwencja fast-xml-parser i xmltodict, a prefiks nigdy nie zderza się z nazwami elementów, ponieważ żadna poprawna nazwa elementu nie zaczyna się od @.

Dlaczego konwersja XML na JSON zachowuje liczby jako ciągi znaków?

Ponieważ konwerter nie wykonuje żadnej koercji typów. Wymuszenie zmiany 01234 na 1234 usunęłoby znaczące zero wiodące z kodów pocztowych, numerów kont i dopełnianych identyfikatorów. Zachowanie każdej wartości jako ciągu znaków to bezpieczne ustawienie domyślne; konwertuj typ świadomie na dalszym etapie, tam gdzie go znasz.

Czy konwersja XML na JSON jest bezstratna?

Nie. Komentarze i instrukcje przetwarzania są odrzucane, semantyka przestrzeni nazw zachowana jest tylko częściowo, a kolejność treści mieszanej może nie przetrwać obiegu w obie strony. Gdy potrzebujesz zachować każdy bajt, użyj Formatowania XML, aby przeformatować XML, zamiast konwertować go na JSON.

Jak obsługiwane są w JSON powtarzane elementy XML?

Pojedynczy element potomny o tej samej nazwie staje się obiektem; dwa lub więcej stają się tablicą. Ponieważ kształt zależy od liczby rodzeństwa, kod konsumenta powinien zawsze normalizować do tablicy, by obsłużyć zarówno przypadek z jednym elementem, jak i z wieloma, bez wykładania się.

Co dzieje się z przestrzeniami nazw XML podczas konwersji na JSON?

Deklaracja xmlns staje się zwykłym kluczem @_xmlns, a prefiks pozostaje wewnątrz ciągu z nazwą elementu, jak w "soap:Body". Semantyczne wiązanie prefiksu z URI nie jest interpretowane, więc odrębne przestrzenie nazw mogą zostać spłaszczone w jedno.

Jak skonwertować JSON z powrotem na XML?

Użyj towarzyszącego Konwertera JSON na XML. Stosuje on te same konwencje @_ i #text w odwrotną stronę, więc atrybuty, treść tekstowa i tablice mapują się z powrotem symetrycznie. Ta symetria jest tym, co umożliwia czysty obieg JSON → XML → JSON w obie strony.

Czy mogę skonwertować XML z wieloma elementami głównymi?

Nie. Wiele elementów na najwyższym poziomie to niepoprawny składniowo XML, więc parser odrzuca dane wejściowe. Najpierw opakuj fragmenty w jeden element główny — zamień <a/><b/> na <root><a/><b/></root> — a następnie konwertuj.

Podsumowanie

Konwersja XML na JSON jest sterowana konwencjami, a nie zmianą formatu. Reguły są spójne między środowiskami uruchomieniowymi: atrybuty mapują się na klucze @_, tekst treści mieszanej na #text, powtarzane rodzeństwo na tablice, a wartości pozostają ciągami znaków, więc zera wiodące i precyzja przetrwają. Pułapki, o których trzeba pamiętać, to zmiana kształtu „jeden kontra tablica”, po cichu pomijane atrybuty oraz utrata komentarzy i semantyki przestrzeni nazw — żadna z nich nie jest błędem, a wszystkie są przewidywalne, gdy znasz stojące za nimi niedopasowanie modeli.

Gdy potrzebujesz szybkiej, prywatnej konwersji, wklej dane do Konwertera XML na JSON — działa w całości w twojej przeglądarce. Najpierw zwaliduj źródło za pomocą Formatowania XML, a w drugą stronę użyj Konwertera JSON na XML, gdy potrzebujesz XML w obiegu w obie strony. Więcej o tym, jak modele formatów danych kształtują zachowanie konwersji, znajdziesz w notatkach o różnicach między YAML a JSON.

Powiązane artykuły

Zobacz wszystkie artykuły