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).
- 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.
| DOM | SAX | StAX | |
|---|---|---|---|
| model | a tree of nodes in memory | callbacks pushed at you | a cursor you pull |
| memory | the whole document, several times its size | constant | constant |
| random access, editing | yes | no | no |
| control flow | yours, after parsing | the parser's | yours |
| use it for | config files, small documents, editing | huge files, simple extraction | huge files, real logic |
XML in the JDK
DOM is the easiest to start with, and has the most famous trap:
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());
}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:
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));
}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:
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);
}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:
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);
}<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.
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).
// 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.
// 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.
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/....
- 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.