DOMDeserializer.java
package tools.jackson.databind.ext;
import java.io.StringReader;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.*;
import org.xml.sax.InputSource;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.deser.std.FromStringDeserializer;
import tools.jackson.databind.util.AccessPattern;
import tools.jackson.databind.util.ClassUtil;
/**
* Base for deserializers that allows parsing DOM values from JSON Strings.
* Nominal type can be {@link org.w3c.dom.Node} or one of the node types that
* {@link DOMSerializer} writes a distinct Type Id for; see
* {@link #findDeserializer} for the full set, and for which types fall back to
* plain {@code Node} handling.
*/
public abstract class DOMDeserializer<T> extends FromStringDeserializer<T>
{
/**
* Name of throw-away root element used for parsing node types that are not
* well-formed XML documents on their own; never part of the result.
*/
private final static String WRAPPER_ELEMENT = "jacksonDomWrapper";
private final static DocumentBuilderFactory DEFAULT_PARSER_FACTORY;
static {
DocumentBuilderFactory parserFactory = DocumentBuilderFactory.newInstance();
// yup, only cave men do XML without recognizing namespaces...
parserFactory.setNamespaceAware(true);
// [databind#1279]: make sure external entities NOT expanded by default
parserFactory.setExpandEntityReferences(false);
// ... and in general, aim for "safety"
try {
parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch(ParserConfigurationException pce) {
// not much point to do anything; could log but...
}
// [databind#2589] add two more settings just in case
try {
parserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
} catch (Exception t) { } // as per previous one, nothing much to do
try {
parserFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
} catch (Exception t) { } // as per previous one, nothing much to do
DEFAULT_PARSER_FACTORY = parserFactory;
}
protected DOMDeserializer(Class<T> cls) { super(cls); }
@Override
public abstract T _deserialize(String value, DeserializationContext ctxt);
/**
* Lookup of deserializer for a DOM {@link Node} subtype. Order matters: the most
* specific type must be checked first, since a check matching a supertype would
* leave the more specific handler unreachable
* (23-Jul-2026, [databind#6113]: `Document` vs `Node`;
* 24-Jul-2026, [databind#6120]: the rest).
*<p>
* Node types with no entry here -- notably {@link Attr}, for which the
* {@code Transformer} emits nothing usable -- fall back to {@code NodeDeserializer},
* which can only produce a {@link Document}. Must stay in sync with the Type Ids
* {@code DOMSerializer} writes.
*
* @since 3.3
*/
public static ValueDeserializer<?> findDeserializer(Class<?> rawType)
{
if (Document.class.isAssignableFrom(rawType)) {
return new DocumentDeserializer();
}
if (Element.class.isAssignableFrom(rawType)) {
return new ElementDeserializer();
}
// `CDATASection` is a subtype of `Text`, so must precede it
if (CDATASection.class.isAssignableFrom(rawType)) {
return new CDATASectionDeserializer();
}
if (Text.class.isAssignableFrom(rawType)) {
return new TextDeserializer();
}
if (Comment.class.isAssignableFrom(rawType)) {
return new CommentDeserializer();
}
if (ProcessingInstruction.class.isAssignableFrom(rawType)) {
return new ProcessingInstructionDeserializer();
}
if (DocumentFragment.class.isAssignableFrom(rawType)) {
return new DocumentFragmentDeserializer();
}
return new NodeDeserializer();
}
protected final Document parse(String value) throws IllegalArgumentException {
try {
return documentBuilder().parse(new InputSource(new StringReader(value)));
} catch (Exception e) {
throw new IllegalArgumentException("Failed to parse JSON String as XML: "+e.getMessage(), e);
}
}
/**
* Helper method for reconstructing node types that are not, by themselves,
* well-formed XML documents (text, comments, processing instructions and so on):
* wraps given XML text in a throw-away root element, parses that, and returns
* the wrapper element whose children are the reconstructed node(s).
*<p>
* NOTE: input is concatenated into XML, but cannot widen what a caller could
* already reach via {@link #parse}: content that closes the wrapper leaves the
* document with 2 root elements, and a {@code DOCTYPE} may not follow a start tag
* -- both are fatal parse errors, on top of the parser factory already disallowing
* DOCTYPE declarations and entity expansion.
*<p>
* NOTE: DOM Level 3 {@code LSParser.parseWithContext()} would do this directly, but
* the JDK's bundled parser does not implement it (throws {@code NOT_SUPPORTED_ERR}),
* so wrapping is the only option that avoids a third-party XML dependency.
*
* @since 3.3
*/
protected final Element parseWrapped(String value) throws IllegalArgumentException {
return parse("<"+WRAPPER_ELEMENT+">"+value+"</"+WRAPPER_ELEMENT+">")
.getDocumentElement();
}
/**
* Overridable factory method used to create {@link DocumentBuilder} for parsing
* XML as DOM.
*
* @since 2.7.6
*/
protected DocumentBuilder documentBuilder() throws ParserConfigurationException {
return DEFAULT_PARSER_FACTORY.newDocumentBuilder();
}
/**
* Helper method for extracting the single child of expected type from wrapper
* element created by {@link #parseWrapped}.
*<p>
* NOTE: only usable for node types with no sub-interface of their own (a
* {@link CDATASection}, for example, would satisfy a {@link Text} check here).
*
* @since 3.3
*/
protected final <N extends Node> N parseWrappedChild(String value, Class<N> expType)
throws IllegalArgumentException
{
Node child = parseWrapped(value).getFirstChild();
if (!expType.isInstance(child)) {
throw new IllegalArgumentException(String.format(
"Failed to parse JSON String as XML %s node: got %s",
ClassUtil.nameOf(expType),
(child == null) ? "no content" : "node type "+child.getNodeType()));
}
return expType.cast(child);
}
/*
/**********************************************************
/* Concrete deserializers
/**********************************************************
*/
@JacksonStdImpl
public static class NodeDeserializer extends DOMDeserializer<Node> {
public NodeDeserializer() { super(Node.class); }
@Override
public Node _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
return parse(value);
}
}
@JacksonStdImpl
public static class DocumentDeserializer extends DOMDeserializer<Document> {
public DocumentDeserializer() { super(Document.class); }
@Override
public Document _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
return parse(value);
}
}
/**
* @since 3.3
*/
@JacksonStdImpl
static class ElementDeserializer extends DOMDeserializer<Element> {
ElementDeserializer() { super(Element.class); }
@Override
public Element _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
// Serialized Element is a well-formed document on its own (Transformer
// hoists any ancestor namespace declarations into the subtree)
return parse(value).getDocumentElement();
}
}
/**
* @since 3.3
*/
@JacksonStdImpl
static class TextDeserializer extends DOMDeserializer<Text> {
TextDeserializer() { super(Text.class); }
// 24-Jul-2026, tatu: [databind#6120] Unlike every other DOM type, the written
// form of a `Text` node IS its (escaped) content, with no delimiters. Default
// trimming would silently drop leading/trailing whitespace -- and turn a
// whitespace-only node (indentation in any pretty-printed document) into
// an empty String, and thereby `null`.
@Override
protected boolean _shouldTrim() { return false; }
// ... and for the same reason empty content is a legal `Text` node, not `null`.
// NOTE: must go via `getEmptyValue()`, which is what `CoercionAction.AsEmpty`
// and `@JsonSetter(contentNulls=AS_EMPTY)` consult (compare to
// `JDKFromStringDeserializer`)
@Override
public Object getEmptyValue(DeserializationContext ctxt) {
return _deserialize("", ctxt);
}
// ... and unlike the immutable scalars `StdScalarDeserializer` assumes, a DOM
// node is mutable and carries a parent pointer (`appendChild()` reparents), so
// the empty value must NOT be shared: `CONSTANT` would have
// `NullsConstantProvider` hand every null the very same node
@Override
public AccessPattern getEmptyAccessPattern() {
return AccessPattern.DYNAMIC;
}
@Override
protected Object _deserializeFromEmptyStringDefault(DeserializationContext ctxt) {
return getEmptyValue(ctxt);
}
@Override
public Text _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
// Cannot just take the first child: parser is allowed to split character
// data into multiple `Text` nodes
Element wrapper = parseWrapped(value);
return wrapper.getOwnerDocument().createTextNode(wrapper.getTextContent());
}
}
/**
* @since 3.3
*/
@JacksonStdImpl
static class CDATASectionDeserializer extends DOMDeserializer<CDATASection> {
CDATASectionDeserializer() { super(CDATASection.class); }
@Override
public CDATASection _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
// Same as with `Text`: content containing "]]>" is written out as multiple
// CDATA sections, so recombine into one
Element wrapper = parseWrapped(value);
return wrapper.getOwnerDocument().createCDATASection(wrapper.getTextContent());
}
}
/**
* @since 3.3
*/
@JacksonStdImpl
static class CommentDeserializer extends DOMDeserializer<Comment> {
CommentDeserializer() { super(Comment.class); }
@Override
public Comment _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
return parseWrappedChild(value, Comment.class);
}
}
/**
* @since 3.3
*/
@JacksonStdImpl
static class ProcessingInstructionDeserializer extends DOMDeserializer<ProcessingInstruction> {
ProcessingInstructionDeserializer() { super(ProcessingInstruction.class); }
@Override
public ProcessingInstruction _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
return parseWrappedChild(value, ProcessingInstruction.class);
}
}
/**
* @since 3.3
*/
@JacksonStdImpl
static class DocumentFragmentDeserializer extends DOMDeserializer<DocumentFragment> {
DocumentFragmentDeserializer() { super(DocumentFragment.class); }
@Override
public DocumentFragment _deserialize(String value, DeserializationContext ctxt) throws IllegalArgumentException {
Element wrapper = parseWrapped(value);
Document doc = wrapper.getOwnerDocument();
DocumentFragment frag = doc.createDocumentFragment();
// NOTE: `appendChild()` detaches from wrapper, so this consumes the list
for (Node child; (child = wrapper.getFirstChild()) != null; ) {
frag.appendChild(child);
}
return frag;
}
}
}