To convert an XML Document to a String in Java, we pass it to a Transformer as a DOMSource and collect the output in a StreamResult that wraps a StringWriter. The same call writes the XML to a file when the StreamResult wraps a file or an output stream instead.
Serialization is needed whenever XML leaves our code, for example to log a SOAP request, send a generated document over HTTP, store a config file after changing it or export data for another system. The javax.xml.transform API in the JDK handles indentation, the XML declaration and the character encoding.
The following example parses a one-line task list and turns the Document back into a String.
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.parse(new InputSource(new StringReader("<tasks><task id=\"1\">Buy milk</task></tasks>")));
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(doc), new StreamResult(writer));
String xml = writer.toString(); // "<tasks><task id="1">Buy milk</task></tasks>"
The method transform() returns nothing, because the text goes into the writer. Next, we wrap the call in a reusable method, go through the output options, fix the blank lines that indentation adds to parsed documents, write files with the right encoding and build a document from scratch.
1. Converting an XML Document to a String With a Transformer
A Transformer applies an XSLT stylesheet to a source tree and writes the result tree. When we create it with newTransformer() and no stylesheet, it performs the identity transform, which copies the source unchanged. Combined with a DOMSource on the input side and a StreamResult on the output side, the identity transform is the JDK’s DOM serializer.

Since the transformer can also load stylesheets and DTDs, we create it from a factory that blocks external access, which is what static analysis tools such as Sonar ask for. The XMLConstants properties ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_STYLESHEET set to an empty string allow no protocol at all. The helper toXmlString() takes the output properties as a map.
static String toXmlString(Node node, Map<String, String> outputProperties) throws TransformerException {
TransformerFactory factory = TransformerFactory.newInstance();
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
Transformer transformer = factory.newTransformer();
outputProperties.forEach(transformer::setOutputProperty);
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(node), new StreamResult(writer));
return writer.toString();
}
The parameter type is Node, not Document, so the same method serializes a single element. A small parse helper with DOCTYPE declarations turned off provides the documents for the examples, as the article on parsing a String to an XML DOM explains in detail.
static Document parseXml(String xml) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
return factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
}
Document doc = parseXml("<tasks><task id=\"1\">Buy milk</task><task id=\"2\">Call mom</task></tasks>");
String full = toXmlString(doc, Map.of());
boolean declaration = full.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?><tasks>"); // true
Element second = (Element) doc.getElementsByTagName("task").item(1);
String one = toXmlString(second, Map.of(OutputKeys.OMIT_XML_DECLARATION, "yes")); // "<task id="2">Call mom</task>"
By default, the output starts with an XML declaration that includes standalone=”no”, followed by the root element on the same line. The next section shows the properties that change it.
2. Output Options With OutputKeys
The OutputKeys class holds the names of the standard output properties from the XSLT xsl:output element. Each one is a String value, such as “yes” or an encoding name, set with setOutputProperty().
| Property | Value | Effect |
|---|---|---|
| OutputKeys.OMIT_XML_DECLARATION | “yes” | Drops the <?xml …?> line, useful for fragments and logs |
| OutputKeys.INDENT | “yes” | Puts each element on its own line, indented by 4 spaces since Java 9 |
| “{http://xml.apache.org/xslt}indent-amount” | “2” | Changes the indentation width of the JDK transformer |
| OutputKeys.ENCODING | “ISO-8859-1” | Sets the declared encoding and, for byte output, the actual encoding |
| OutputKeys.METHOD | “xml”, “html”, “text” | Selects the output method, where xml is the default |
| OutputKeys.DOCTYPE_SYSTEM | A DTD URI | Adds a DOCTYPE declaration to the output |
The standalone=”no” part has no output key that removes it on its own. It comes from the Document, so we call doc.setXmlStandalone(true) before transforming, and the transformer leaves the attribute out.
Document doc = parseXml("<tasks><task id=\"1\">Buy milk</task></tasks>");
doc.setXmlStandalone(true);
String xml = toXmlString(doc, Map.of()); // "<?xml version="1.0" encoding="UTF-8"?><tasks><task id="1">Buy milk</task></tasks>"
3. Pretty Printing Without Blank Lines
With INDENT set to “yes”, the transformer adds line breaks and indentation. On a document built in code, the result looks clean. On a document parsed from a file that was already indented, every line break of the original file is still in the DOM as a whitespace-only text node, and since Java 9 the transformer keeps those nodes and adds its own indentation around them.
Document doc = parseXml("<tasks>\n <task id=\"1\">Buy milk</task>\n <task id=\"2\">Call mom</task>\n</tasks>");
Map<String, String> pretty = Map.of(OutputKeys.INDENT, "yes",
"{http://xml.apache.org/xslt}indent-amount", "2");
String messy = toXmlString(doc, pretty);
long blankLines = messy.lines().filter(String::isBlank).count(); // 3
The printed String shows the problem.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<tasks>
<task id="1">Buy milk</task>
<task id="2">Call mom</task>
</tasks>
The fix removes the whitespace-only text nodes before transforming. The XPath expression //text()[normalize-space()=”] finds them, and each one is removed from its parent. For data-oriented XML like ours, those nodes carry no information.
static void stripWhitespaceNodes(Document doc) throws XPathExpressionException {
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList blanks = (NodeList) xpath.evaluate("//text()[normalize-space()='']", doc, XPathConstants.NODESET);
for (int i = 0; i < blanks.getLength(); i++) {
Node blank = blanks.item(i);
blank.getParentNode().removeChild(blank);
}
}
Document doc = parseXml("<tasks>\n <task id=\"1\">Buy milk</task>\n <task id=\"2\">Call mom</task>\n</tasks>");
Map<String, String> pretty = Map.of(OutputKeys.INDENT, "yes",
"{http://xml.apache.org/xslt}indent-amount", "2");
stripWhitespaceNodes(doc);
String clean = toXmlString(doc, pretty);
long blankLines = clean.lines().filter(String::isBlank).count(); // 0
long lineCount = clean.lines().count(); // 5
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<tasks>
<task id="1">Buy milk</task>
<task id="2">Call mom</task>
</tasks>
We strip whitespace-only text nodes only in data XML, never in mixed content such as XHTML, where a space between two inline elements is part of the text. The change in Java 9 is tracked as JDK-8087303, and it also changed the default indentation from 0 to 4 spaces.
4. Writing the XML Document to a File
To write a file, we give the StreamResult an OutputStream and let the transformer encode the bytes. The transformer uses the ENCODING property for both the declaration and the bytes, so the two always agree. A Writer also works, but the writer decides the bytes, and a mismatch with the declared encoding produces a file that other parsers read incorrectly.
Say a task app exports a user’s list as tasks.xml every night, and a desktop client imports it. The export opens the file in a try-with-resources block, so the file handle is closed even when the transform fails, which on Windows otherwise leaves the file locked.
Document doc = parseXml("<tasks><task id=\"1\">Buy milk</task><task id=\"2\">Call mom</task></tasks>");
Path file = Files.createTempFile("tasks", ".xml");
TransformerFactory factory = TransformerFactory.newInstance();
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
Transformer transformer = factory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
try (OutputStream out = Files.newOutputStream(file)) {
transformer.transform(new DOMSource(doc), new StreamResult(out));
}
String firstLine = Files.readAllLines(file).get(0); // "<?xml version="1.0" encoding="UTF-8" standalone="no"?>"
int lineCount = Files.readAllLines(file).size(); // 5
A StreamResult also accepts a File, as in new StreamResult(file.toFile()). The transformer opens and closes the stream itself in that case, which is shorter, but we lose control over options such as appending or creating parent folders. The methods of the Java IO API cover those file operations.
5. Building a Document From Scratch and Serializing It
A Document does not have to come from parsing. We create an empty one with newDocument() and add nodes with the factory methods of the Document, such as createElement(). The serializer escapes special characters, so text with & or < needs no manual escaping.
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element tasks = doc.createElement("tasks");
doc.appendChild(tasks);
Element task = doc.createElement("task");
task.setAttribute("id", "3");
task.setTextContent("Milk & <eggs>");
tasks.appendChild(task);
String xml = toXmlString(doc, Map.of(OutputKeys.OMIT_XML_DECLARATION, "yes")); // "<tasks><task id="3">Milk & <eggs></task></tasks>"
Building XML by string concatenation, as in “<task>” + name + “</task>”, breaks as soon as a value contains one of those characters, and with user input it lets the user inject elements. The DOM route avoids both problems, and its output can go through the same helper for indentation.
6. Serializing With LSSerializer
The DOM Level 3 LSSerializer is a second serializer in the JDK. It comes from the DOMImplementationLS feature of the document and has a writeToString() method, so no writer object is needed.
Document doc = parseXml("<tasks><task id=\"1\">Buy milk</task></tasks>");
DOMImplementationLS ls = (DOMImplementationLS) doc.getImplementation();
LSSerializer serializer = ls.createLSSerializer();
String xml = serializer.writeToString(doc); // "<?xml version="1.0" encoding="UTF-16"?><tasks><task id="1">Buy milk</task></tasks>"
serializer.getDomConfig().setParameter("xml-declaration", false);
String noDecl = serializer.writeToString(doc); // "<tasks><task id="1">Buy milk</task></tasks>"
The declaration says UTF-16, because a Java String holds UTF-16 characters. If that String is later written to a file as UTF-8, the declaration is wrong, so we either drop it or write through an LSOutput with setEncoding(“UTF-8”). The Transformer is more widely documented and handles both strings and files with one set of options, which makes it the usual choice.
| Transformer | LSSerializer | |
|---|---|---|
| One-line String result | No, needs a StringWriter | Yes, writeToString() |
| Declared encoding for String output | UTF-8 by default | UTF-16 |
| Pretty printing | INDENT plus indent-amount | format-pretty-print parameter |
| Can apply XSLT stylesheets | Yes | No |
7. Reusing a Transformer Safely
A Transformer is not thread-safe, so two threads must not call transform() on the same instance at the same time. For the identity transform, creating a transformer per call costs little, as in our helper. When a real stylesheet is involved, we compile it once into Templates, which is thread-safe, and call newTransformer() on it per request.
TransformerFactory factory = TransformerFactory.newInstance();
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
String identity = """
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:template match="@*|node()"><xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy></xsl:template>
</xsl:stylesheet>""";
Templates templates = factory.newTemplates(new StreamSource(new StringReader(identity)));
Document doc = parseXml("<tasks><task id=\"1\">Buy milk</task></tasks>");
StringWriter writer = new StringWriter();
templates.newTransformer().transform(new DOMSource(doc), new StreamResult(writer));
String xml = writer.toString(); // "<tasks><task id="1">Buy milk</task></tasks>"
A shared Templates object fits a web service that formats every response with the same stylesheet, because the expensive compilation runs once at startup.
8. XML to String FAQs
Serializing is the last step before XML leaves the app, so these questions deal with what the receiver sees.
8.1. Why Is My XML Output on One Line?
The transformer does not indent by default. We set OutputKeys.INDENT to “yes”, and optionally indent-amount, as in section 3. A one-line output is still valid XML and is the better choice for logs and network messages, since it is smaller.
8.2. Does the Transformer Keep CDATA Sections?
Yes, when the DOM contains CDATASection nodes, the output keeps them as <![CDATA[…]]>. A document parsed with default settings keeps CDATA nodes, whereas a factory with setCoalescing(true) turns them into plain text, which the transformer escapes. The property OutputKeys.CDATA_SECTION_ELEMENTS forces CDATA output for the text of named elements.
8.3. Why Do Accented Characters Look Wrong in My XML File?
The bytes and the declared encoding disagree. That happens when a Writer with one charset writes output that declares another one, such as a Windows-1252 FileWriter under a UTF-8 declaration. Writing through an OutputStream, as in section 4, lets the transformer use the same encoding for both.
8.4. How Do I Write a JDOM2 Document to a String?
JDOM2 has its own serializer, XMLOutputter, with Format.getPrettyFormat() for indentation and outputString(document) for the String. The article on reading XML with JDOM2 shows it at the end.
9. Conclusion
Converting an XML Document to a String in Java uses the identity Transformer, with a DOMSource for the input and a StreamResult over a StringWriter for the output. The same call writes a file when the StreamResult wraps an OutputStream, and the ENCODING property controls both the declaration and the bytes.
Output properties drop the declaration and turn on indentation, setXmlStandalone(true) removes standalone=”no”, and stripping whitespace-only text nodes avoids blank lines in pretty-printed output since Java 9. For reading documents in the first place, see the DOM parser tutorial.
10. References
- Transformer Javadoc (Java 25)
- OutputKeys Javadoc
- Templates Javadoc
- LSSerializer Javadoc
- XSLT 1.0, Output (xsl:output)
Happy Learning !!
Thank you this really helped. :-)
What if my xml file is not in Document format?
how to remove the standalone=”no” in xml file
I am trying to launch browsers using xml file. How to call stored browser values in xml in selenium? Please help