StAX (Streaming API for XML) is the JDK pull parser in javax.xml.stream, which means our code asks for the next XML event in a loop, and the parser reads only as far as we ask, without building the whole document in memory. StAX gives us two ways to read, the cursor API with XMLStreamReader and the iterator API with XMLEventReader, plus XMLStreamWriter for writing XML.
We use the Java StAX parser for large XML files such as product feeds, bank statements or database exports, and whenever we need only a few values from a file and want to stop reading once we have them.
The following example reads every song title from a playlist.xml file with the cursor API on Java 25. The first two lines create a factory that refuses DTDs, which blocks XML external entity (XXE) attacks.
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
List<String> titles = new ArrayList<>();
try (InputStream in = Files.newInputStream(Path.of("playlist.xml"))) {
XMLStreamReader reader = factory.createXMLStreamReader(in);
while (reader.hasNext()) {
if (reader.next() == XMLStreamConstants.START_ELEMENT
&& reader.getLocalName().equals("title")) {
titles.add(reader.getElementText());
}
}
reader.close();
}
int count = titles.size(); // 3
String first = titles.getFirst(); // "Morning Run"
Notice that the loop decides when to move forward with next(), and getElementText() reads the text of the current element in one call. We look at the event model first, map the whole file to Java records with both APIs, compare them, lock down the factory against XXE, and write XML with XMLStreamWriter.
1. Pull Parsing with javax.xml.stream
A streaming parser reads XML from top to bottom and reports what it finds as events, such as a start tag, some text or an end tag. With SAX, the parser drives the loop and calls our handler methods for every event (push). With StAX, our code drives the loop and pulls the next event when it is ready, the same way we move a JDBC ResultSet or an Iterator forward (pull).

Because we own the loop, a method can read one part of the document and return, and another method can continue with the next part. We can also stop with return or break once we have what we need, whereas a SAX handler has to throw an exception to stop the parser. The DOM vs SAX vs StAX comparison covers the trade-offs of all three JDK parsers, and the SAX parser tutorial shows the push model in detail.
Every call to next() returns an int constant from XMLStreamConstants. These five event types cover almost every document we read.
| Constant | Value | The cursor is on |
|---|---|---|
| START_ELEMENT | 1 | A start tag such as <song id=”1″>, with its name and attributes |
| END_ELEMENT | 2 | An end tag such as </song> |
| CHARACTERS | 4 | Text, including the whitespace between tags |
| START_DOCUMENT | 7 | The XML declaration, before the first next() call |
| END_DOCUMENT | 8 | The end of the input; hasNext() returns false afterward |
The examples read this playlist.xml file. Each song has an id attribute and three child elements.
<?xml version="1.0" encoding="UTF-8"?>
<playlist name="Road trip">
<song id="1">
<title>Morning Run</title>
<artist>Lena</artist>
<seconds>215</seconds>
</song>
<song id="2">
<title>Late Train</title>
<artist>Omar</artist>
<seconds>187</seconds>
</song>
<song id="3">
<title>Blue Sky</title>
<artist>Lena</artist>
<seconds>240</seconds>
</song>
</playlist>
2. Reading XML with the Cursor API (XMLStreamReader)
The cursor API is the lower-level of the two. An XMLStreamReader points at one event at a time, and we call methods such as getLocalName(), getAttributeValue() or getText() on the reader itself to read the current event. No object is created per event, so the cursor produces less garbage on big files.
The following example maps each <song> element to a Song record. The method keeps the values of the current song in local variables and creates the record when it reaches the </song> end tag. The factory comes from secureFactory(), which we build in section 5.
record Song(int id, String title, String artist, int seconds) {}
static List<Song> readWithCursor(Path file) throws IOException, XMLStreamException {
List<Song> songs = new ArrayList<>();
try (InputStream in = Files.newInputStream(file)) {
XMLStreamReader reader = secureFactory().createXMLStreamReader(in);
try {
int id = 0, seconds = 0;
String title = null, artist = null;
while (reader.hasNext()) {
int event = reader.next();
if (event == XMLStreamConstants.START_ELEMENT) {
switch (reader.getLocalName()) {
case "song" -> id = Integer.parseInt(reader.getAttributeValue(null, "id"));
case "title" -> title = reader.getElementText();
case "artist" -> artist = reader.getElementText();
case "seconds" -> seconds = Integer.parseInt(reader.getElementText().strip());
default -> { }
}
} else if (event == XMLStreamConstants.END_ELEMENT
&& reader.getLocalName().equals("song")) {
songs.add(new Song(id, title, artist, seconds));
}
}
} finally {
reader.close();
}
}
return songs;
}
List<Song> songs = readWithCursor(Path.of("playlist.xml"));
int count = songs.size(); // 3
Song first = songs.getFirst(); // Song[id=1, title=Morning Run, artist=Lena, seconds=215]
int totalSeconds = songs.stream().mapToInt(Song::seconds).sum(); // 642
A few details in this code matter in every StAX program.
- The method getAttributeValue(null, “id”) reads an attribute by its local name; the first argument is the namespace URI, and null means it is not checked. It returns null when the attribute is missing.
- The method getElementText() concatenates all text inside a text-only element, even when the parser splits it into several CHARACTERS events, and leaves the cursor on the matching end tag. It throws XMLStreamException if the element contains a child element.
- The interface XMLStreamReader does not extend AutoCloseable, so it cannot go into try-with-resources. We close it in a finally block, and we close the InputStream separately, because reader.close() does not close the underlying stream.
- We compare element names with equals(), because XML names are case-sensitive, so <Title> and <title> are different elements.
The reader methods are valid only for some event types. Calling getLocalName() while the cursor is on text throws IllegalStateException, so we check the event type before reading.
XMLStreamReader reader = secureFactory().createXMLStreamReader(new StringReader("<song>Late Train</song>"));
int firstEvent = reader.next(); // 1 (START_ELEMENT)
int secondEvent = reader.next(); // 4 (CHARACTERS)
String text = reader.getText(); // "Late Train"
String name = reader.getLocalName(); // IllegalStateException
3. Reading XML with the Iterator API (XMLEventReader)
The iterator API wraps every event in an immutable XMLEvent object. We get the next one with nextEvent(), check its type with isStartElement() or isEndElement(), and convert it with asStartElement() or asEndElement(). Since an event is an object, we can store it, pass it to another method or look at the next one with peek() without consuming it.
The following example reads the same file into the same records with an XMLEventReader. The attribute comes from getAttributeByName(), which takes a QName, and the text comes from getElementText() on the reader.
static List<Song> readWithIterator(Path file) throws IOException, XMLStreamException {
List<Song> songs = new ArrayList<>();
QName idName = new QName("id");
try (InputStream in = Files.newInputStream(file)) {
XMLEventReader reader = secureFactory().createXMLEventReader(in);
try {
int id = 0, seconds = 0;
String title = null, artist = null;
while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
if (event.isStartElement()) {
StartElement start = event.asStartElement();
switch (start.getName().getLocalPart()) {
case "song" -> id = Integer.parseInt(start.getAttributeByName(idName).getValue());
case "title" -> title = reader.getElementText();
case "artist" -> artist = reader.getElementText();
case "seconds" -> seconds = Integer.parseInt(reader.getElementText().strip());
default -> { }
}
} else if (event.isEndElement()
&& event.asEndElement().getName().getLocalPart().equals("song")) {
songs.add(new Song(id, title, artist, seconds));
}
}
} finally {
reader.close();
}
}
return songs;
}
List<Song> songs = readWithIterator(Path.of("playlist.xml"));
int count = songs.size(); // 3
String lastTitle = songs.getLast().title(); // "Blue Sky"
Many older examples read text with (Characters) reader.nextEvent() right after a start tag. That cast fails with a ClassCastException when the element is empty, such as <title/>, or when a comment comes first, and it returns only part of the text when the parser splits it. The method getElementText() handles all three cases.
The iterator API also supports filters. A filtered reader returns only the events that pass an EventFilter, which is a functional interface, so a lambda works.
List<String> ids = new ArrayList<>();
try (InputStream in = Files.newInputStream(Path.of("playlist.xml"))) {
XMLInputFactory factory = secureFactory();
XMLEventReader songsOnly = factory.createFilteredReader(factory.createXMLEventReader(in),
e -> e.isStartElement() && e.asStartElement().getName().getLocalPart().equals("song"));
while (songsOnly.hasNext()) {
StartElement song = songsOnly.nextEvent().asStartElement();
ids.add(song.getAttributeByName(new QName("id")).getValue());
}
songsOnly.close();
}
String allIds = String.join(",", ids); // "1,2,3"
4. XMLStreamReader or XMLEventReader?
Both APIs read the same stream with the same parser, so the choice depends on how the rest of our code wants to use the events.
| Cursor API | Iterator API | |
|---|---|---|
| Main type | XMLStreamReader | XMLEventReader |
| Next event | next() returns an int | nextEvent() returns an XMLEvent |
| Reading data | Methods on the reader, valid for the current event only | Methods on the event object, valid at any time |
| Look ahead | No | peek() |
| Keep or pass events | No, the reader moves on | Yes, events are immutable |
| Objects per event | None | One XMLEvent per event |
| Matching writer | XMLStreamWriter | XMLEventWriter, which can copy events from a reader |
We pick the cursor API for plain reading of large files, and the iterator API when events must be stored, inspected ahead with peek() or copied to an XMLEventWriter. For example, a filter that removes one element from an XML file and writes the rest unchanged is a few lines with XMLEventReader and XMLEventWriter, because every event we do not drop is written as is.
5. Securing XMLInputFactory Against XXE
An XML document can declare a DTD with an external entity that points to a file or URL. If the parser resolves it, the content of that file ends up in our data, which is called an XML external entity (XXE) attack. In Java 25, a new XMLInputFactory still supports DTDs and external entities by default, so a document uploaded by a user can read files from our server.
The following example parses a document whose entity x points to a local file named secret.txt. The helper textOf() collects all text of a document, and secureFactory() turns off DTD support.
static XMLInputFactory secureFactory() {
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
return factory;
}
static String textOf(XMLInputFactory factory, String xml) throws XMLStreamException {
XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(xml));
StringBuilder text = new StringBuilder();
while (reader.hasNext()) {
if (reader.next() == XMLStreamConstants.CHARACTERS) {
text.append(reader.getText());
}
}
reader.close();
return text.toString();
}
String untrusted = """
<?xml version="1.0"?>
<!DOCTYPE song [<!ENTITY x SYSTEM "file:secret.txt">]>
<song>&x;</song>""";
String leaked = textOf(XMLInputFactory.newInstance(), untrusted); // the content of secret.txt
String blocked = textOf(secureFactory(), untrusted); // XMLStreamException: The entity "x" was referenced, but not declared.
With SUPPORT_DTD set to false, the parser skips the DTD, so the entity is never declared and parsing stops with an XMLStreamException. We also set IS_SUPPORTING_EXTERNAL_ENTITIES to false for parsers that still read DTDs. To apply the same rule to every parser in the JVM, including libraries we do not control, we start Java with -Djdk.xml.dtd.support=deny, a property described in the java.xml module summary. The OWASP XXE prevention cheat sheet lists the settings for the other JDK parsers.
6. Stopping Early in a Large XML File
Say a music app gets a nightly catalog export from a partner as one XML file with a few million songs, and a support tool needs the title of one song by its id. Loading that file into a DOM tree would need many times the file size in heap. With StAX, the tool reads one event at a time and returns as soon as it finds the song.
The following example returns the title of the song with a given id and leaves the rest of the file unread. It uses nextTag(), which skips whitespace and comments and moves to the next start or end tag.
static Optional<String> titleOf(Path file, String wantedId) throws IOException, XMLStreamException {
try (InputStream in = Files.newInputStream(file)) {
XMLStreamReader reader = secureFactory().createXMLStreamReader(in);
try {
while (reader.hasNext()) {
if (reader.next() == XMLStreamConstants.START_ELEMENT
&& reader.getLocalName().equals("song")
&& wantedId.equals(reader.getAttributeValue(null, "id"))) {
reader.nextTag(); // moves to <title>
return Optional.of(reader.getElementText());
}
}
return Optional.empty();
} finally {
reader.close();
}
}
}
Optional<String> second = titleOf(Path.of("playlist.xml"), "2"); // Optional[Late Train]
Optional<String> missing = titleOf(Path.of("playlist.xml"), "9"); // Optional.empty
The method relies on <title> being the first child of <song>. When the order is not fixed, we loop until the end tag of the song and check each child name, as the cursor example in section 2 does.
7. Writing XML with XMLStreamWriter
StAX also writes XML, which SAX cannot do. An XMLStreamWriter writes start tags, attributes, text and end tags in order, and it escapes special characters such as & and < in text and attribute values.
StringWriter out = new StringWriter();
XMLStreamWriter writer = XMLOutputFactory.newInstance().createXMLStreamWriter(out);
writer.writeStartDocument("UTF-8", "1.0");
writer.writeStartElement("playlist");
writer.writeAttribute("name", "Gym");
writer.writeStartElement("song");
writer.writeAttribute("id", "4");
writer.writeStartElement("title");
writer.writeCharacters("Rock & Roll");
writer.writeEndElement();
writer.writeEndDocument(); // closes <song> and <playlist>
writer.close();
String xml = out.toString(); // <?xml version="1.0" encoding="UTF-8"?><playlist name="Gym"><song id="4"><title>Rock & Roll</title></song></playlist>
The writer does not indent its output. When a person needs to read the file, we write the newlines and spaces with writeCharacters(), or we write the document with DOM and a Transformer, as shown in the write XML to a file tutorial.
8. StAX Parser FAQs
Developers who move from DOM or SAX to StAX mostly ask about speed, attributes and the errors that getElementText() throws.
8.1. Is StAX part of the JDK?
Yes. StAX was defined by JSR 173 and added to the JDK in Java 6. In Java 9 and later it is in the java.xml module, together with DOM, SAX and XPath, so no extra dependency is needed. Alternative implementations, such as Woodstox, plug in through XMLInputFactory.newFactory().
8.2. Is StAX faster than SAX or DOM?
StAX and SAX are both streaming parsers with low, constant memory use, and they are in the same range for speed, so the choice between them is about code style, pull or push. Both are much lighter than DOM for large files, because DOM keeps every node in memory. For a small configuration file, DOM or XPath is simpler, and the speed difference does not matter.
8.3. How do we read an attribute value with StAX?
With the cursor API, we call reader.getAttributeValue(null, “id”) while the cursor is on a START_ELEMENT event. With the iterator API, we call startElement.getAttributeByName(new QName(“id”)), which returns an Attribute or null, and read its value with getValue().
8.4. Why does getElementText() throw “expects text only element”?
The method getElementText() works only for elements that contain text and no child elements. If we call it on <song>, which contains <title>, it throws an XMLStreamException with that message. We call it on the leaf elements, such as <title>, and loop over the children of container elements.
8.5. Can StAX validate XML against an XSD schema?
Yes. A javax.xml.validation.Validator accepts a StAXSource that wraps an XMLStreamReader, and it reports the first error as a SAXException, for example when <seconds> contains abc but the schema says xs:int. Older articles that say StAX has no validation are out of date.
9. Conclusion
StAX lets our code pull XML events one at a time. The cursor API with XMLStreamReader is the lean choice for reading large files, and the iterator API with XMLEventReader gives us event objects that we can keep, peek at and copy to a writer.
In both APIs, getElementText() is the safe way to read text, and the factory needs SUPPORT_DTD set to false before it reads XML from outside our app. For reading only part of a big file, StAX is the simplest JDK option, because we can return as soon as we have the value.
10. References
- javax.xml.stream package (Java 25)
- XMLInputFactory Javadoc (Java 25)
- XMLStreamReader Javadoc (Java 25)
- Why StAX? (Oracle JAXP tutorial)
- OWASP XML External Entity Prevention Cheat Sheet
Happy Learning !!
Hi Lokesh,
Can you provide me sample program to read xml data which is wrapped in html tags and write the data to text file please.
Thanks in advance.
You can do this in following steps:
1. Read and parse the HTML using Jsoup. [Jsoup Example]
2. Unescape the XML. I am assuming the XML within the HTML will be escaped.
3. Read the XML as shown in this tutorial.
Document doc = Jsoup.parse(html); Element xmlContainer = doc.getElementById("xmlContentDiv"); String escapedXml = xmlContainer.text(); String xmlContent = org.apache.commons.text.StringEscapeUtils.unescapeHtml4(escapedXml); try (StringReader stringReader = new StringReader(xmlContent)) { XMLInputFactory factory = XMLInputFactory.newInstance(); XMLStreamReader xmlReader = factory.createXMLStreamReader(stringReader); // Read XML content while (xmlReader.hasNext()) { int event = xmlReader.next(); if (event == XMLStreamConstants.START_ELEMENT || event == XMLStreamConstants.CHARACTERS) { System.out.println(xmlReader.getText()); } //Custom logic } } catch (Exception e) { e.printStackTrace(); }