skip to content
everything.io.jsonAndXml

json and xml

parsing and writing both, plus the new json api on the way.

chapter
chapter 10.09
reading time
7 min read

the JDK has had XML built in for twenty years (java.xml): a DOM tree, SAX push events, a StAX pull cursor, plus XPath and XSLT. it has no JSON API you can use today: everyone uses a library, usually Jackson, which maps JSON straight onto your records.

that's changing: a small standard JSON API is incubating in jdk 28 (JEP 540).

the picture

you know how you can read a book by photocopying the whole thing first, by having someone read it aloud to you, or by turning the pages yourself? those are the three ways to parse a document. DOM photocopies it (the whole tree in memory, then you browse). SAX reads it aloud (it calls you, event by event, and doesn't stop). StAX hands you the book (you call next() when you're ready).

api =
<order id="42">
<item qty="2">chai</item>
<item qty="1">samosa</item>
</order>
  1. parse(input, handler): the parser drives.

SAX pushes events at your handler as it reads. nothing is kept in memory, so it handles files of any size.

step 1/9
pick an API and step through the same document. the amber line is where the parser is. watch who calls whom: SAX calls your handler, your StAX loop calls the reader, and DOM doesn't let your code run until it's finished.
DOMSAXStAX
modela tree of nodes in memorycallbacks pushed at youa cursor you pull
memorythe whole document, several times its sizeconstantconstant
random access, editingyesnono
control flowyours, after parsingthe parser'syours
use it forconfig files, small documents, editinghuge files, simple extractionhuge files, real logic

XML in the JDK

DOM is the easiest to start with, and has the most famous trap:

Main.java
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;
import java.io.ByteArrayInputStream;
 
void main() throws Exception {
    String xml = """
        <order id="42">
          <item qty="2">chai</item>
          <item qty="1">samosa</item>
        </order>
        """;
    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
        .parse(new ByteArrayInputStream(xml.getBytes()));
 
    Element order = doc.getDocumentElement();
    IO.println("order " + order.getAttribute("id"));
    NodeList items = order.getElementsByTagName("item");
    for (int i = 0; i < items.getLength(); i++) {
        Element item = (Element) items.item(i);
        IO.println("  " + item.getTextContent() + " x" + item.getAttribute("qty"));
    }
    IO.println("children of <order>: " + order.getChildNodes().getLength());
}
output
order 42
  chai x2
  samosa x1
children of <order>: 5

two items, five children. the line breaks and indentation between tags are text nodes too: whitespace, item, whitespace, item, whitespace. code that does getFirstChild() expecting an element gets a newline instead. use getElementsByTagName, check getNodeType(), or use XPath, which ignores all that:

Main.java
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import java.io.ByteArrayInputStream;
 
void main() throws Exception {
    String xml = "<order id=\"42\"><item qty=\"2\">chai</item><item qty=\"1\">samosa</item></order>";
    var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes()));
    XPath xpath = XPathFactory.newInstance().newXPath();
    IO.println(xpath.evaluate("sum(/order/item/@qty)", doc));
    IO.println(xpath.evaluate("/order/item[@qty > 1]", doc));
}
output
3
chai

for big inputs, StAX keeps memory flat and still reads like normal code. getElementText() reads everything up to the matching end tag in one call:

Main.java
import javax.xml.stream.*;
import java.io.StringReader;
 
void main() throws Exception {
    String xml = "<order id=\"42\"><item qty=\"2\">chai</item><item qty=\"1\">samosa</item></order>";
    XMLStreamReader r = XMLInputFactory.newInstance().createXMLStreamReader(new StringReader(xml));
    int total = 0;
    while (r.hasNext()) {
        int event = r.next();                                   // you pull the next event
        if (event == XMLStreamConstants.START_ELEMENT && r.getLocalName().equals("item")) {
            int qty = Integer.parseInt(r.getAttributeValue(null, "qty"));
            String name = r.getElementText();                   // reads up to </item>
            IO.println(name + " x" + qty);
            total += qty;
        }
    }
    IO.println("total items: " + total);
}
output
chai x2
samosa x1
total items: 3

and for writing, never build XML with string concatenation, for the same reason you don't build SQL that way: & and < in your data. a writer escapes for you:

Main.java
import javax.xml.stream.*;
import java.io.StringWriter;
 
void main() throws Exception {
    var out = new StringWriter();
    XMLStreamWriter w = XMLOutputFactory.newInstance().createXMLStreamWriter(out);
    w.writeStartElement("note");
    w.writeAttribute("from", "ada & co");
    w.writeCharacters("1 < 2, obviously");
    w.writeEndElement();
    w.close();
    IO.println(out);
}
output
<note from="ada & co">1 < 2, obviously</note>

JSON: use a library

there's no JSON parser in java 25. the ecosystem filled the gap long ago: Jackson (the default in Spring), Gson, and the Jakarta specs JSON-P (tree and streaming) and JSON-B (binding). they all offer the same three levels as XML: a streaming parser, a tree (JsonNode), and data binding, which is what you use 90% of the time: JSON in, your record out.

order.json
Order.java

placed_at: names differ (placed_at vs placedAt), so the record says @JsonProperty("placed_at"). the string becomes an Instant (built into Jackson 3; a separate module in 2.x).

click a line on either side. data binding matches JSON keys to record components by name, recursing into nested objects and lists. the unknown key is the part that changes between Jackson versions.
OrderJson.java
// Jackson 3 (tools.jackson), the current major version. not part of the JDK
JsonMapper mapper = JsonMapper.builder().build();          // immutable, thread-safe: make one
 
Order order = mapper.readValue(json, Order.class);         // JSON → record
String out = mapper.writeValueAsString(order);             // record → JSON
 
JsonNode tree = mapper.readTree(json);                     // or no classes at all
int qty = tree.get("items").get(0).get("qty").asInt();

records and data binding fit perfectly: a record's components are its JSON shape, the canonical constructor validates on the way in, and there's no setter soup. Jackson has handled records since 2.12. Jackson 3 (October 2025) moved to the tools.jackson packages, requires java 17, made mappers immutable, and builds in java.time support.

the JSON API on its way

JEP 540 adds a deliberately small JSON API as an incubator module in jdk 28 (in development, due march 2027). it parses RFC 8259 JSON into a tree of immutable values and writes them back. that's it: no data binding, no streaming, both listed as non-goals, so libraries aren't going anywhere. the point is that a script or a small tool can read JSON without adding a dependency.

Weather.java
// jdk 28, incubating: needs --add-modules jdk.incubator.json. not runnable on 25
JsonValue json = Json.parse(body);
json.get("properties").get("periods").asList().stream()
    .mapToInt(p -> p.get("temperature").asInt())
    .average()
    .ifPresent(IO::println);

JsonValue is a sealed interface with six subtypes (JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull), so it plays well with pattern matching. malformed input throws an unchecked JsonParseException that says where it went wrong. as an incubator API it can still change before it's final.

one level deeper:why there are so many XML APIs+

XML arrived in java in pieces. JAXP (java 1.4, 2002) bundled DOM and SAX, the two W3C-era standards. StAX (JSR 173) came in java 6, after people got tired of writing SAX state machines. JAXB, the XML data binding, was in the JDK from 6 to 10 and was removed in java 11 along with the rest of the Java EE modules (JEP 320), so on modern java it's a dependency (jakarta.xml.bind). the JDK's parser itself is a repackaged Apache Xerces, which is why the feature names are http://apache.org/xml/features/....

keylines
  • DOM is a tree in memory, SAX pushes events at you, StAX lets you pull them.
  • DOM counts whitespace between tags as text nodes. XPath sidesteps it.
  • build XML and JSON with a writer or a library, never with string concatenation.
  • untrusted XML: disallow DOCTYPE, or XXE reads your files for the sender.
  • JSON means a library today (Jackson). bind to records, never let input pick classes.
  • a small standard JSON API is incubating in jdk 28 (JEP 540): tree only, no binding.