Java Convert String to XML Document (DOM, Secure Parsing)

Java convert String to XML Document with DocumentBuilder and InputSource. Secure parsing, malformed input, prolog errors, encodings and reuse explained.

A String passing through StringReader and InputSource into DocumentBuilder.parse, producing a Document tree with a forecast root element and two day elements

To convert a String to an XML Document in Java, we wrap the String in a StringReader and an InputSource, and pass it to DocumentBuilder.parse(), which returns an org.w3c.dom.Document. The classes are part of the JDK, in javax.xml.parsers and org.xml.sax, so no library is needed.

We parse XML from a String when the XML arrives as text rather than as a file, such as the body of a REST or SOAP call, a message from a queue, a column in a database table or a test fixture. Once we have a Document, we can read it with the DOM methods or XPath, change it and write it out again.

The following example parses a short weather forecast held in a text block.

String xml = """
        <forecast city="Pune">
          <day date="2026-10-11" high="31" low="22"/>
          <day date="2026-10-12" high="29" low="21"/>
        </forecast>""";

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xml)));

String root = doc.getDocumentElement().getTagName();                  // "forecast"
String city = doc.getDocumentElement().getAttribute("city");          // "Pune"
int days = doc.getElementsByTagName("day").getLength();               // 2

Notice the disallow-doctype-decl feature, which makes the parser reject any DOCTYPE declaration and with it the external entities that attackers hide in XML input. We look at each class in the chain, build a reusable parse method, handle malformed input and encodings, and compare the DOM with the alternatives for turning text into Java objects.

1. Classes That Convert a String to an XML Document

A DocumentBuilder reads XML from a source and builds the DOM tree, which is a tree of Node objects with the Document at the top. It has no parse(String xml) method for XML content, so a String needs two small adapters before the parser can read it.

A String passing through StringReader and InputSource into DocumentBuilder.parse, producing a Document tree with a forecast root element and two day elements
The String becomes a character stream, the InputSource hands it to the parser, and the parser returns the DOM tree
ClassPackageRole in the conversion
DocumentBuilderFactoryjavax.xml.parsersHolds the parser settings, such as namespace awareness and security features, and creates builders
DocumentBuilderjavax.xml.parsersParses a source and returns a Document
StringReaderjava.ioTurns the String into a character stream
InputSourceorg.xml.saxWraps the stream so the parser accepts it, and can carry a system id for error messages
Documentorg.w3c.domThe parsed tree, with getDocumentElement() as the entry point

The method parse(String) exists, but its argument is a URI, not XML content. Passing XML text to it is a common mistake, and the parser tries to open the text as an address.

DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document wrong = builder.parse("<forecast city=\"Pune\"/>");     // MalformedURLException: no protocol

2. A Reusable and Secure Parse Method

In a real app, the conversion goes into one method that every caller uses. That method is the right place for the security settings, because XML from outside the app can contain a DOCTYPE with external entities. When the parser expands such an entity, it reads a local file or calls a URL and puts the result into the document. The attack is called XML External Entity (XXE) injection, and on JDK 25 the default DocumentBuilderFactory still expands external entities from files.

static Document toDocument(String xml) throws ParserConfigurationException, SAXException, IOException {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
    factory.setNamespaceAware(true);
    DocumentBuilder builder = factory.newDocumentBuilder();
    builder.setErrorHandler(new DefaultHandler());
    return builder.parse(new InputSource(new StringReader(xml)));
}

The method also turns on namespace awareness, which XPath queries with prefixes and getElementsByTagNameNS() need, and it replaces the default error handler. Without that handler, the JDK parser prints a [Fatal Error] line to System.err before it throws, which clutters the logs of a server. DefaultHandler rethrows fatal errors and prints nothing.

String withDoctype = "<!DOCTYPE forecast [<!ENTITY c \"Pune\">]><forecast city=\"&c;\"/>";
Document blocked = toDocument(withDoctype);     // SAXParseException: DOCTYPE is disallowed

Say a logistics company accepts shipment notices as XML over HTTP, and a REST endpoint passes the request body to toDocument(). Legitimate partners never send a DOCTYPE, so rejecting it costs nothing, and a crafted request that tries to read /etc/passwd fails before any entity is resolved. Every parser that reads XML from outside the app gets disallow-doctype-decl set to true. On Java 22 and later, factory.setAttribute(“jdk.xml.dtd.support”, “deny”) does the same through a JDK property.

3. Reading Values From the Parsed Document

The entry point of the tree is getDocumentElement(), which returns the root element. Some older examples use getFirstChild() instead, and that breaks as soon as the XML starts with a comment or a processing instruction, because those are children of the Document too.

Document doc = toDocument("<!-- generated --><forecast city=\"Pune\"/>");

String firstChild = doc.getFirstChild().getNodeName();             // "#comment"
String rootElement = doc.getDocumentElement().getNodeName();       // "forecast"

From the root, we read attributes with getAttribute() and find elements with getElementsByTagName(). The next example turns the forecast into Java records, which is what the rest of an app wants to work with.

record Day(LocalDate date, int high, int low) {}
Document doc = toDocument("""
        <forecast city="Pune">
          <day date="2026-10-11" high="31" low="22"/>
          <day date="2026-10-12" high="29" low="21"/>
        </forecast>""");

List<Day> forecast = new ArrayList<>();
NodeList dayNodes = doc.getElementsByTagName("day");
for (int i = 0; i < dayNodes.getLength(); i++) {
    Element day = (Element) dayNodes.item(i);
    forecast.add(new Day(LocalDate.parse(day.getAttribute("date")),
            Integer.parseInt(day.getAttribute("high")),
            Integer.parseInt(day.getAttribute("low"))));
}
Day first = forecast.get(0);           // Day[date=2026-10-11, high=31, low=22]

For deeper documents, XPath is shorter than nested loops, and the article on getting attribute values with XPath uses the same kind of Document. The DOM parser tutorial covers text nodes, whitespace and walking the whole tree.

4. Handling Malformed XML Strings

A String that is not well-formed XML makes parse() throw a SAXParseException. The exception carries the line and column of the problem, which is what we log or return to the caller, because “invalid XML” alone does not help anyone fix the sender.

String broken = "<forecast><day></forecast>";

String problem;
try {
    toDocument(broken);
    problem = "none";
} catch (SAXParseException e) {
    problem = e.getLineNumber() + ":" + e.getColumnNumber() + " " + e.getMessage();
}
String report = problem;   // "1:18 The element type "day" must be terminated by the matching end-tag "</day>"."

Two messages come from text before the root element rather than from broken tags. Both are common when XML is cut out of a log file, read with the wrong charset or built by string concatenation.

Input starts withMessageFix
Any text, such as x<forecast/>Content is not allowed in prolog.Remove the text before the first <
A byte order mark (BOM) character \uFEFFContent is not allowed in prolog.Strip the BOM, or parse the original bytes so the parser handles it
Whitespace before <?xml …?>The processing instruction target matching “[xX][mM][lL]” is not allowed.Call strip() on the String, since the declaration must come first
String padded = "   <?xml version=\"1.0\"?><forecast city=\"Pune\"/>";

Document fails = toDocument(padded);                 // SAXParseException: The processing instruction target matching "[xX][mM][lL]" is not allowed.
Document works = toDocument(padded.strip());
String city = works.getDocumentElement().getAttribute("city");     // "Pune"

For a quick validity check, such as a form field that accepts XML, we wrap toDocument() in a method that returns false on SAXException. A well-formed document can still be wrong for our app, and checking it against an XSD is the next step for that.

5. Parsing Bytes Instead of a String

A String in Java is already decoded text, so the encoding attribute in its XML declaration has no effect when we parse through a StringReader. When the XML arrives as bytes, from a file, a socket or an HTTP body, we pass the bytes to the parser as an InputStream. The parser reads the declaration and decodes the bytes with the encoding it names.

byte[] latin1 = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><city>Z\u00fcrich</city>"
        .getBytes(StandardCharsets.ISO_8859_1);

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

String fromBytes = builder.parse(new ByteArrayInputStream(latin1)).getDocumentElement().getTextContent();
boolean correct = fromBytes.equals("Z\u00fcrich");                                  // true

String decodedWrong = new String(latin1, StandardCharsets.UTF_8);
String fromString = builder.parse(new InputSource(new StringReader(decodedWrong))).getDocumentElement().getTextContent();
boolean broken = fromString.equals("Z\u00fcrich");                                  // false

The second attempt decodes the Latin-1 bytes as UTF-8 before parsing, so the letter \u00fc turns into a replacement character, and the declaration cannot repair it later. The rule is to convert bytes to a String only when the XML is text in our app, and otherwise to let the parser decode it.

The method parse() has overloads for other sources too, so bytes, files and streams go in without a String in between.

XML comes fromCall
A Stringbuilder.parse(new InputSource(new StringReader(xml)))
A byte array or HTTP bodybuilder.parse(new ByteArrayInputStream(bytes))
A filebuilder.parse(path.toFile())
A classpath resourcebuilder.parse(getClass().getResourceAsStream(“/forecast.xml”))
A URLbuilder.parse(“https://…”), after checking that the URL is trusted

6. Reusing Factories and Builders

Creating a DocumentBuilderFactory looks up the implementation class, which costs more than the parse of a small String. An app that converts thousands of messages per minute, such as a queue consumer, keeps one configured factory and creates builders from it. Neither DocumentBuilderFactory nor DocumentBuilder is guaranteed to be thread-safe, so a builder belongs to one thread at a time, and reset() restores it to its original settings between documents.

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
ThreadLocal<DocumentBuilder> builders = ThreadLocal.withInitial(() -> {
    try {
        return factory.newDocumentBuilder();
    } catch (ParserConfigurationException e) {
        throw new IllegalStateException(e);
    }
});

DocumentBuilder builder = builders.get();
builder.reset();
Document doc = builder.parse(new InputSource(new StringReader("<forecast city=\"Oslo\"/>")));
String city = doc.getDocumentElement().getAttribute("city");    // "Oslo"

The factory is configured once, before any thread uses it, and each thread gets its own builder from the ThreadLocal. For most apps, a new factory and builder per call, as in toDocument(), is fast enough and simpler.

7. DOM Compared With Other Ways to Read an XML String

A DOM Document is the right result when we need to search, change or re-serialize the XML. When we only want the data in Java objects, or the String is very large, other APIs fit better.

APIResultGood for
DOM (DocumentBuilder)Document tree in memorySearching with XPath, changing and writing XML back
JAXB (Jakarta XML Binding)Annotated Java objectsFixed schemas mapped to classes
Jackson XMLPOJOs or recordsApps that already use Jackson for JSON
JDOM2JDOM Document with Java collectionsA friendlier tree API than DOM
StAXStream of eventsLarge documents that do not fit in memory

8. String to XML Document FAQs

Most surprises with XML strings happen in the steps right before and right after the conversion.

8.1. How Do I Convert the Document Back to a String?

We use a Transformer with a DOMSource and a StreamResult over a StringWriter. The article on converting an XML Document to a String shows indentation, the XML declaration and writing to a file.

8.2. Do I Need setNamespaceAware(true) to Parse a String?

Only when the XML uses namespaces and we query it by namespace. The factory is not namespace-aware by default, and the parse works either way, but XPath with prefixes and getElementsByTagNameNS() find nothing in a document parsed without it, as the article on XPath namespace resolution explains.

8.3. Why Does My Parsed Document Show Up as [#document: null]?

That is the toString() output of the DOM implementation, not an empty document. The Document node has no value, so it prints null. We check the content with getDocumentElement().getTagName() or serialize the document to see the XML.

8.4. Can DocumentBuilder Parse an HTML String?

Only if the HTML is well-formed XHTML. Ordinary HTML has unclosed tags such as <br>, which make the XML parser throw a SAXParseException. For HTML, we use an HTML parser such as jsoup.

9. Conclusion

Converting a String to an XML Document in Java takes one chain of calls, builder.parse(new InputSource(new StringReader(xml))). The factory behind the builder decides how safe and how useful the result is, so it gets disallow-doctype-decl for any input from outside the app and namespace awareness when the XML uses namespaces.

A malformed String throws a SAXParseException with a line and column, text before the root element causes the prolog errors, and bytes belong in an InputStream so the parser can honor the declared encoding. With the Document in hand, getDocumentElement() is the starting point for reading values.

10. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I’m getting FileNotFoundException in the below step
    Document doc = builder.parse(new InputSource(new StringReader(xmlString)));

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.