Java SAX Parser: Read XML with DefaultHandler (Examples)

Read XML with the Java SAX parser and DefaultHandler, buffer characters() text correctly, block XXE, validate with XSD and stop parsing early on Java 25.

Sequence of SAX callbacks for one recipe element, namely startElement recipe with the id attribute, startElement name, characters called three times for Mac, the ampersand and Cheese, endElement name, and endElement recipe

A SAX parser (Simple API for XML) reads an XML document from top to bottom and calls methods of our handler class, such as startElement(), characters() and endElement(), for every tag and every piece of text it finds. The parser keeps no tree in memory, so a Java SAX parser reads a 5 MB file and a 5 GB file with the same small amount of heap.

We use SAX to read large XML files in a single pass, for example a product feed, a log export or the sheets inside an Excel .xlsx file, and to pull a few values out of a document without building a DOM tree first.

The following example reads all recipe names from a recipes.xml file with SAXParserFactory and an anonymous DefaultHandler on Java 25. The setFeature() call rejects any DOCTYPE, which blocks XML external entity (XXE) attacks.

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

List<String> names = new ArrayList<>();
factory.newSAXParser().parse(Path.of("recipes.xml").toFile(), new DefaultHandler() {
    private final StringBuilder text = new StringBuilder();

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        text.setLength(0);
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        text.append(ch, start, length);
    }

    @Override
    public void endElement(String uri, String localName, String qName) {
        if (qName.equals("name")) {
            names.add(text.toString());
        }
    }
});
int count = names.size();                     // 3
String third = names.get(2);                  // "Mac & Cheese"

Notice that characters() only appends to a buffer, and the name is read in endElement(). That pattern is the core of every correct SAX handler, and the sections cover why, how to map the file to Java records, how to secure and validate the parser, how to report errors and how to stop parsing early.

1. Events and Callbacks in a SAX Parser

SAX is a push parser. Once we call parse(), the parser owns the loop. It reads the input in order and, for each part of the document, calls the matching method of the handler we passed in. Our code never asks for the next element; it reacts to what the parser reports.

Sequence of SAX callbacks for one recipe element, namely startElement recipe with the id attribute, startElement name, characters called three times for Mac, the ampersand and Cheese, endElement name, and endElement recipe
SAX calls our handler for every tag and text chunk in document order; one text value can arrive in several characters() calls

In most code, the handler is a subclass of DefaultHandler, which implements ContentHandler, ErrorHandler, DTDHandler and EntityResolver with empty methods. We override only the callbacks we need.

CallbackCalled when the parser readsTypical use
startDocument()The start of the inputReset state
startElement(uri, localName, qName, attributes)A start tag such as <recipe id=”1″>Read attributes, clear the text buffer
characters(ch, start, length)Text, in one or more chunksAppend to a StringBuilder
endElement(uri, localName, qName)An end tag such as </recipe>Use the collected text, finish an object
endDocument()The end of the inputFinal checks
error() and fatalError()A validation error or malformed XMLReport or stop

SAX was created by David Megginson and the XML-DEV mailing list in 1998, and Java has shipped it with JAXP since J2SE 1.4. Today it is part of the java.xml module, next to DOM and the StAX parser.

2. Mapping XML to Java Records with DefaultHandler

Reading a whole file into objects means keeping some state between callbacks, because the id comes in startElement() of <recipe>, the name and the minutes come later in their own elements, and the recipe is complete only at </recipe>.

The following example reads this recipes.xml file into Recipe records. Each recipe has an id attribute and two child elements, and the third name contains the entity &amp;.

<?xml version="1.0" encoding="UTF-8"?>
<recipes>
  <recipe id="1">
    <name>Pancakes</name>
    <minutes>20</minutes>
  </recipe>
  <recipe id="2">
    <name>Tomato Soup</name>
    <minutes>35</minutes>
  </recipe>
  <recipe id="3">
    <name>Mac &amp; Cheese</name>
    <minutes>25</minutes>
  </recipe>
</recipes>
record Recipe(int id, String name, int minutes) {}
class RecipeHandler extends DefaultHandler {
    private final List<Recipe> recipes = new ArrayList<>();
    private final StringBuilder text = new StringBuilder();
    private int id;
    private int minutes;
    private String name;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        text.setLength(0);
        if (qName.equals("recipe")) {
            id = Integer.parseInt(attributes.getValue("id"));
        }
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        text.append(ch, start, length);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        switch (qName) {
            case "name" -> name = text.toString().strip();
            case "minutes" -> minutes = Integer.parseInt(text.toString().strip());
            case "recipe" -> recipes.add(new Recipe(id, name, minutes));
            default -> { }
        }
    }

    List<Recipe> recipes() {
        return recipes;
    }
}

We parse the file with a parser from secureParser(), which we build in section 4. The method parse(File, DefaultHandler) opens and closes the file for us.

RecipeHandler handler = new RecipeHandler();
secureParser().parse(Path.of("recipes.xml").toFile(), handler);
List<Recipe> recipes = handler.recipes();
int count = recipes.size();                                               // 3
Recipe third = recipes.get(2);                                            // Recipe[id=3, name=Mac & Cheese, minutes=25]
long quick = recipes.stream().filter(r -> r.minutes() <= 25).count();     // 2

The handler reads the attribute by name with attributes.getValue(“id”), which returns null when the attribute is missing. Reading by position, such as getValue(0), breaks as soon as someone adds a second attribute before id. A new handler instance is needed for every parse, because it keeps the list and the buffer as fields.

3. Why characters() Returns Text in Pieces

A parser may return one text value in a single characters() call or split it into several chunks. The JDK parser splits text at entity references such as &amp; and at the edges of its internal buffer, which depend on the file size. A handler that treats each call as the full value works in tests and loses data in production.

The class LastChunkHandler shows the mistake, because it keeps only the text of the last characters() call inside <name>.

class LastChunkHandler extends DefaultHandler {
    final List<String> names = new ArrayList<>();
    private boolean inName;
    private String last;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        inName = qName.equals("name");
    }

    @Override
    public void characters(char[] ch, int start, int length) {
        if (inName) {
            last = new String(ch, start, length);
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName) {
        if (qName.equals("name")) {
            names.add(last);
        }
        inName = false;
    }
}
LastChunkHandler wrong = new LastChunkHandler();
secureParser().parse(Path.of("recipes.xml").toFile(), wrong);
String first = wrong.names.get(0);            // "Pancakes"
String broken = wrong.names.get(2);           // " Cheese"

We always append text to a StringBuilder in characters(), clear it in startElement() and read it in endElement(). The RecipeHandler from section 2 does that, so it returns “Mac & Cheese” for the same file.

4. Secure SAXParserFactory Settings

A DOCTYPE in an XML file can declare an external entity that points to a file on our server or to a URL. The JDK parser resolves such entities by default, even in Java 25, so a SAX parser that reads uploaded XML can be used to read server files. That attack is called XML external entity (XXE) injection.

The method secureParser() turns on three settings. The first rejects every document that contains a DOCTYPE, the second enables the JAXP secure processing limits, and the third makes the parser namespace-aware.

static SAXParser secureParser() throws ParserConfigurationException, SAXException {
    SAXParserFactory factory = SAXParserFactory.newInstance();
    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
    factory.setNamespaceAware(true);
    return factory.newSAXParser();
}
String untrusted = """
        <?xml version="1.0"?>
        <!DOCTYPE recipe [<!ENTITY x SYSTEM "file:secret.txt">]>
        <recipe>&x;</recipe>""";
secureParser().parse(new InputSource(new StringReader(untrusted)), new DefaultHandler());   // SAXParseException: DOCTYPE is disallowed

Most data files never need a DTD, so rejecting the DOCTYPE costs nothing. If a partner format does contain a DOCTYPE, we keep it but turn off the features http://xml.org/sax/features/external-general-entities and http://xml.org/sax/features/external-parameter-entities, as the OWASP XXE prevention cheat sheet describes.

Namespace awareness changes what the handler receives. By default, SAXParserFactory is not namespace-aware, and the parser passes an empty localName and puts the element name only in qName. With setNamespaceAware(true), localName holds the name without a prefix, and uri holds the namespace.

Element in the fileNamespace-awarelocalNameqName
<recipe>No (default)“” (empty)“recipe”
<recipe>Yes“recipe”“recipe”
<r:recipe xmlns:r=”…”>Yes“recipe”“r:recipe”

Code that compares localName on a default factory never matches anything, which is a common reason for a handler that “does nothing”. Our handlers compare qName, which works in both modes for files without prefixes.

5. Validation Errors, Line Numbers and ErrorHandler

The parser reports two kinds of problems. A fatal error means the XML is not well-formed, for example an end tag is missing, and the parser always stops with a SAXParseException that carries the line and column. A recoverable error is a validation error, such as a value that breaks the schema, and DefaultHandler.error() ignores it.

String notWellFormed = "<recipes><recipe id=\"4\"><name>Toast</recipe></recipes>";
secureParser().parse(new InputSource(new StringReader(notWellFormed)), new RecipeHandler());   // SAXParseException: The element type "name" must be terminated by the matching end-tag "</name>".

For validation, we attach an XSD schema to the factory with setSchema(). The file bad-recipes.xml has <minutes>-5</minutes>, but the schema declares the element as xs:positiveInteger. The handler subclass overrides error() and rethrows the exception, because without that override the invalid file is parsed as if nothing happened.

static SAXParser validatingParser(Path xsd) throws ParserConfigurationException, SAXException {
    Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(xsd.toFile());
    SAXParserFactory factory = SAXParserFactory.newInstance();
    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    factory.setNamespaceAware(true);
    factory.setSchema(schema);
    return factory.newSAXParser();
}
class StrictRecipeHandler extends RecipeHandler {
    @Override
    public void error(SAXParseException e) throws SAXException {
        throw e;
    }
}
RecipeHandler lenient = new RecipeHandler();
validatingParser(Path.of("recipes.xsd")).parse(Path.of("bad-recipes.xml").toFile(), lenient);
int parsed = lenient.recipes().size();        // 1, the error was ignored

validatingParser(Path.of("recipes.xsd")).parse(Path.of("bad-recipes.xml").toFile(), new StrictRecipeHandler());   // SAXParseException: cvc-minInclusive-valid: Value '-5' is not facet-valid with respect to minInclusive '1' for type 'positiveInteger'.

The exception message contains the line number, and e.getLineNumber() and e.getColumnNumber() return it as numbers, so an import job can log the line and column where a partner file is broken.

6. Stopping a SAX Parser Early

SAX has no method to stop the parser, because the parser owns the loop. The standard way is to throw a SAXException subclass from a callback and catch that subclass around parse(). The parser stops reading at once and passes our exception through.

Say a meal-planning app gets a nightly feed with thousands of recipes, and a preview screen needs only the first recipe that takes longer than 30 minutes. The following handler throws StopParsing as soon as it finds one, so the rest of the feed is never read.

class StopParsing extends SAXException {
    StopParsing() {
        super("match found");
    }
}
class FirstSlowRecipe extends RecipeHandler {
    String found;
    int seen;

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        super.endElement(uri, localName, qName);
        if (qName.equals("recipe")) {
            seen++;
            Recipe last = recipes().getLast();
            if (last.minutes() > 30) {
                found = last.name();
                throw new StopParsing();
            }
        }
    }
}
FirstSlowRecipe finder = new FirstSlowRecipe();
try {
    secureParser().parse(Path.of("recipes.xml").toFile(), finder);
} catch (StopParsing done) {
    // expected, the handler found a match
}
String found = finder.found;                  // "Tomato Soup"
int seen = finder.seen;                       // 2

The handler saw only two of the three recipes. When stopping early is a common need, the pull model of StAX is simpler, because the loop can return without an exception.

7. SAX Compared with DOM and StAX

The JDK has three standard ways to read XML, and SAX is the oldest streaming one. The DOM vs SAX vs StAX article compares them in depth.

SAXStAXDOM
ModelPush, the parser calls our handlerPull, our loop asks for eventsTree of nodes in memory
MemorySmall and constantSmall and constantGrows with the document
Random access and XPathNoNoYes
Writes XMLNoYesYes, with a Transformer
Stop earlyThrow an exceptionreturn or breakNot applicable, the file is already parsed

We choose SAX for large files that we read once from start to end, especially when a library already gives us a SAX source, such as Apache POI for reading large Excel files. For small configuration files, reading XML with DOM or XPath needs less code.

8. SAX Parser FAQs

Most SAX questions start with a handler that compiles but collects nothing, or with old code built on deprecated classes.

8.1. What is the difference between localName and qName in startElement()?

The qName argument is the name as written in the file, including a prefix such as r:recipe. The localName argument is the name without the prefix, and the parser fills it only when the factory is namespace-aware. With the default factory, localName is an empty string.

8.2. Is XMLReaderFactory deprecated?

Yes. XMLReaderFactory is deprecated since Java 9, and SAXParserFactory replaces it. When we need the lower-level XMLReader, for example to set a ContentHandler and an ErrorHandler separately, we get it from saxParser.getXMLReader().

8.3. Is SAXParser thread-safe?

No. A SAXParser is not guaranteed to work when two threads use it at the same time. We create one parser per thread or per parse, and a handler with fields also needs a new instance per parse. Creating a parser is cheap compared with reading a large file.

8.4. How do we read CDATA sections with SAX?

The text of a CDATA section arrives in characters() like any other text, so the StringBuilder pattern reads it without changes. If we need to know where the CDATA section starts and ends, we extend org.xml.sax.ext.DefaultHandler2 and override startCDATA() and endCDATA() from LexicalHandler.

8.5. Can a SAX parser read XML from a String?

Yes. We wrap the string in an InputSource with a StringReader, as in parser.parse(new InputSource(new StringReader(xml)), handler). For a file, we pass a File, an InputStream or a URI string.

9. Conclusion

A SAX parser pushes events to a handler as it reads, so it handles files of any size with little memory. A correct handler buffers text from characters() in a StringBuilder and reads it in endElement(), because a single value can arrive in several chunks.

We create the parser from a SAXParserFactory that rejects DOCTYPE declarations, override error() when we validate against a schema, and throw a custom SAXException to stop early. For code that needs to stop often or write XML, StAX is the better fit.

10. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. Need an help ….have used SAX-reader but for the description field for eg Test & return values
    the reader doesn’t read the complete value but reads value after &.

    i.e. it displays “return values” only

  2. <work>
                <ratings_sum type="integer">26798</ratings_sum>
                <ratings_count type="integer">7324</ratings_count>
            </work>
    

    using your method, how would you suggest i parse the info for ratings sum

  3. <isbn><![CDATA[ 0062380001 ]]></isbn>
    

    i’m trying to parse some data and i’m trying to get the isbn (above). i am aware that you deal with cdata within the part of the characters function where the value.length == 0, so do you have any idea i can access and obtain the isbn?

  4. Hi

    I want a generic solution for this . Means let say User has changed the xml , no need to those attributes in the Pojo class and i will run and will get correct output. Also anways suppose i will eliminate that Pojo also its very good . Bcoz i dont know what will be the xml format . So that it will work for the all xml input file . I want to go for only SAX parser.

    Thanks
    Tony

  5. Sax will support to read XML file with multiple root elements? XML file has multiple encoding declarions for every root element?

      • If we see below xml which has “multiple encoding declarations” for each “Contact” element..The is my target xml got from client. So please suggest me how to proceed with below formatted file.

        00000009151666829922

        1

        00000009151666829922

        1

        While reading getting error: The processing instruction target matching “[xX][mM][lL]” is not allowed

        Thanks for your quick response.

    • Try this

      
      URL oracle = new URL(&quot;http://www.oracle.com/&quot;);
      BufferedReader in = new BufferedReader(new InputStreamReader(oracle.openStream()));
      
      String inputLine;
      while ((inputLine = in.readLine()) != null)
             System.out.println(inputLine);
      in.close();
      
      
  6. Its a nice try, but SAX means parse on fly. but you holds the data into some Stacks and Arrays.. seems its not a good arch. idea

  7. hello can u plz me out by telling how parse any xml file without knowing the elements i mean the tags of xml.. jst knowing the xml file source… i want to parse webservices available on net jst by giving url n get the content of that xml content which is returned by the webservice. so how to get it done. plz help me

    • interesting question. what you are planning to do with data without knowing what you are getting? programmatically it’s possible, but I can not think of why somebody would like to do this.

      • example

        for example above only tag seperator will be same and inside content can vary, this is wen you are not sure about input
        how can we parse to read only the content between tag_seprator

  8. Nice article Lokesh. Is there any possibility to get single employee detail alone based on “id” attribute?

    Thanks,
    Sasi

  9. Hello, Thank you very much for your tutorial. I am learning SAX parsing at the same time that I have a very basic kwoledge of Java. I’ve been fiercely googling around trying to make sense of this error but ArrayList is driving me crazy.
    Can you tell me why your UsersXmlParser.java is throwing this compile error (I copy and paste your source code):

    ( just in case is relevant, I am writing in Linux )

    $ javac model.java UserParserHandler.java UsersXmlParser.java TestSAXParser.java
    UsersXmlParser.java:17: illegal start of type
    ArrayList users = new ArrayList();
    ^
    UsersXmlParser.java:17: ‘(‘ or ‘[‘ expected
    ArrayList users = new ArrayList();
    ^
    UsersXmlParser.java:17: illegal start of expression
    ArrayList users = new ArrayList();
    ^
    UsersXmlParser.java:17: illegal start of expression
    ArrayList users = new ArrayList();
    ^
    4 errors

    —
    thank you again!
    Andres

  10. Hey nice tutorial! However I spot a little bug in the sample code paste on this page: in the xml file, value node “firstname” and “lastname” should be changed to “firstName” and “lastName”, otherwise the firstname and lastname value will never be set to the User object. Anyway great tutorial buddy!

  11. Excellent article. Thanks. I’ve been trying to wrap my head around an XML parsing example I found elsewhere and it wasn’t making sense. Yours is the cleanest, clearest example I’ve found. Thanks a bunch.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.