SAX Parser in Java: Event-Driven Streaming XML Parsing

SAX (Simple API for XML) is a push-based streaming parser. Instead of building a tree in memory, the parser fires events (startElement, endElement, characters) as it scans through the document. Your handler receives each event and decides what to do with it. SAX uses constant memory regardless of document size, making it ideal for parsing gigabyte-scale XML files.

Share

How SAX Works: Push-Based Event Model

Event methodFired when
startDocument()Parsing begins
endDocument()Parsing completes
startElement(uri, localName, qName, attrs)Opening tag encountered
endElement(uri, localName, qName)Closing tag encountered
characters(char[], start, length)Text content between tags
ignorableWhitespace(char[], start, length)Whitespace between elements
startPrefixMapping(prefix, uri)Namespace declaration encountered
fatalError(SAXParseException)Fatal parse error
SAX does not build any tree. There is no way to navigate backward or access sibling elements. Your handler must maintain its own state (e.g., a stack, current element name) to track context.

Writing a SAX ContentHandler

Java
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.util.*;

public class BookSaxHandler extends DefaultHandler {

    private final List<Map<String, String>> books = new ArrayList<>();
    private Map<String, String> current;
    private String currentTag;
    private final StringBuilder text = new StringBuilder();

    // Called when an opening tag is encountered
    @Override
    public void startElement(String uri, String localName, String qName,
                             Attributes attrs) {
        currentTag = qName;
        text.setLength(0);   // reset text buffer

        if ("book".equals(qName)) {
            current = new HashMap<>();
            current.put("id", attrs.getValue("id"));
            current.put("genre", attrs.getValue("genre"));
        }
    }

    // Called with text content — may be called multiple times for one element!
    @Override
    public void characters(char[] ch, int start, int length) {
        text.append(ch, start, length);
    }

    // Called when a closing tag is encountered
    @Override
    public void endElement(String uri, String localName, String qName) {
        if (current != null) {
            switch (qName) {
                case "title"  -> current.put("title",  text.toString().trim());
                case "author" -> current.put("author", text.toString().trim());
                case "year"   -> current.put("year",   text.toString().trim());
                case "book"   -> { books.add(current); current = null; }
            }
        }
        text.setLength(0);
    }

    public List<Map<String, String>> getBooks() { return books; }

    // Main: wire up the parser
    public static void main(String[] args) throws Exception {
        SAXParserFactory factory = SAXParserFactory.newInstance();
        factory.setNamespaceAware(true);
        SAXParser parser = factory.newSAXParser();

        BookSaxHandler handler = new BookSaxHandler();
        parser.parse(new java.io.File("library.xml"), handler);

        handler.getBooks().forEach(b ->
            System.out.printf("Book %s: %s by %s%n",
                    b.get("id"), b.get("title"), b.get("author")));
    }
}
The characters() callback may be called multiple times for a single text node: always accumulate into a StringBuilder and read the result only in endElement().

Error Handling: ErrorHandler Interface

Java
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;

public class StrictErrorHandler implements ErrorHandler {

    @Override
    public void warning(SAXParseException e) {
        System.err.printf("WARNING at line %d col %d: %s%n",
                e.getLineNumber(), e.getColumnNumber(), e.getMessage());
    }

    @Override
    public void error(SAXParseException e) throws SAXException {
        // Recoverable error — throw to abort, or log to continue
        throw e;
    }

    @Override
    public void fatalError(SAXParseException e) throws SAXException {
        // Non-recoverable — must throw
        throw e;
    }
}

// Register on the XMLReader
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
XMLReader reader = saxParser.getXMLReader();
reader.setErrorHandler(new StrictErrorHandler());
reader.setContentHandler(new BookSaxHandler());
reader.parse(new InputSource(new FileInputStream("library.xml")));

Streaming Large XML Files: Memory-Efficient Extraction

SAX shines when you need to extract a small subset of data from a very large XML file. This example counts books without loading them all into memory.

Java
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.*;
import javax.xml.parsers.*;
import java.io.*;

public class LargeXmlProcessor {

    /** Count <book> elements in a potentially very large file. */
    public static int countBooks(File xmlFile) throws Exception {
        int[] count = {0};   // effectively-final wrapper

        SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
        parser.parse(xmlFile, new DefaultHandler() {
            @Override
            public void startElement(String uri, String localName,
                                     String qName, Attributes attrs) {
                if ("book".equals(qName)) count[0]++;
            }
        });

        return count[0];
    }

    /** Early exit: throw a special exception to stop parsing once target found. */
    public static String findFirstTitle(InputStream xmlStream) throws Exception {
        String[] found = {null};
        try {
            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
            parser.parse(xmlStream, new DefaultHandler() {
                boolean inTitle = false;
                StringBuilder sb = new StringBuilder();

                @Override
                public void startElement(String u, String l, String q, Attributes a) {
                    inTitle = "title".equals(q);
                    sb.setLength(0);
                }

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

                @Override
                public void endElement(String u, String l, String q) throws SAXException {
                    if ("title".equals(q)) {
                        found[0] = sb.toString().trim();
                        throw new SAXException("STOP");  // abort parsing
                    }
                }
            });
        } catch (SAXException e) {
            if (!"STOP".equals(e.getMessage())) throw e;
        }
        return found[0];
    }
}
Throwing a SAXException from a handler callback is the standard technique for early termination. The parser stops immediately and the exception propagates to the caller where you can catch it by its message.

Security: Disable XXE in SAX

Java
SAXParserFactory factory = SAXParserFactory.newInstance();

// Prevent XXE attacks
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

SAXParser parser = factory.newSAXParser();
parser.parse(untrustedInput, new MyHandler());

SAX vs StAX: When to Choose Each

AspectSAX (push)StAX (pull)
ControlParser drives: calls your handlerYou drive: call next()
Early exitThrow exceptionSimply stop calling next()
Write XMLNoYes: XMLStreamWriter
ComplexityMedium: must track state manuallyLower: logic flows naturally
PerformanceSlightly faster (no pull overhead)Similar
Best forSimple streaming extractionComplex conditional parsing logic