From 6f448c482f9407313bffdb4f426b5bf4673b2cc5 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Tue, 28 Jul 2026 21:32:31 +0530 Subject: [PATCH 1/3] Apply `XmlNameProcessor` to unwrapped `ObjectNode` root element name --- .../dataformat/xml/ser/ToXmlGenerator.java | 22 +++++++-- .../xml/ser/XmlSerializationContext.java | 4 +- .../xml/node/JsonNodeSerUnwrapped441Test.java | 46 +++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java index 5096e0297..79de2da64 100644 --- a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java +++ b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java @@ -518,6 +518,23 @@ public final boolean setNextNameIfMissing(QName name) return false; } + /** + * Method for running configured {@link XmlNameProcessor} over a name that comes + * from content being written (like {@code Map} keys or {@code ObjectNode} property + * names), as opposed to statically known POJO property names. + * Needed by callers that have to construct the {@link QName} themselves instead + * of going through {@link #writeName(String)}. + * + * @since 3.3 + */ + public QName encodeContentName(String namespaceURI, String localName) + { + _nameToEncode.namespace = (namespaceURI == null) ? "" : namespaceURI; + _nameToEncode.localPart = localName; + _nameProcessor.encodeName(_nameToEncode); + return new QName(_nameToEncode.namespace, _nameToEncode.localPart); + } + /** * Methdod called when a structured (collection, array, map) is being * output. @@ -613,10 +630,7 @@ public JsonGenerator writeName(String name) throws JacksonException } else { // Should this ever get called? ns = (_nextName == null) ? "" : _nextName.getNamespaceURI(); - _nameToEncode.namespace = ns; - _nameToEncode.localPart = name; - _nameProcessor.encodeName(_nameToEncode); - setNextName(new QName(_nameToEncode.namespace, _nameToEncode.localPart)); + setNextName(encodeContentName(ns, name)); } return this; } diff --git a/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java b/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java index b89d7d48a..a0a69bff3 100644 --- a/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java +++ b/src/main/java/tools/jackson/dataformat/xml/ser/XmlSerializationContext.java @@ -275,8 +275,10 @@ protected void _serializeUnwrappedObjectNode(ToXmlGenerator xgen, Object value, Map.Entry entry = root.properties().iterator().next(); final JsonNode newRoot = entry.getValue(); + // Name comes from content, so has to go through the same XmlNameProcessor + // the generator applies to other content-derived names. // No namespace associated with JsonNode: - _initWithRootName(xgen, new QName(entry.getKey())); + _initWithRootName(xgen, xgen.encodeContentName("", entry.getKey())); if (ser == null) { ser = findTypedValueSerializer(newRoot.getClass(), true); } diff --git a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java index 35821214e..2ca5ac734 100644 --- a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java @@ -9,6 +9,11 @@ import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; +import tools.jackson.dataformat.xml.XmlFactory; +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.dataformat.xml.XmlNameProcessor; +import tools.jackson.dataformat.xml.XmlNameProcessors; +import tools.jackson.dataformat.xml.XmlReadFeature; import tools.jackson.dataformat.xml.XmlTestUtil; import tools.jackson.dataformat.xml.XmlWriteFeature; @@ -98,4 +103,45 @@ public void testNodeAsProperty() throws Exception XML_MAPPER.writeValueAsString(stuff)); */ } + + // The unwrapped root name comes from content, so it needs the same + // XmlNameProcessor treatment the name gets when written as a child element. + @Test + public void testUnwrappedRootNameIsProcessed() throws Exception + { + final String BAD_NAME = "$ I am ! &;"; + XmlMapper mapper = mapperWith(XmlNameProcessors.newReplacementProcessor()); + + ObjectNode oneProp = mapper.createObjectNode(); + oneProp.putObject(BAD_NAME).put("id", 13); + + ObjectNode twoProps = mapper.createObjectNode(); + twoProps.putObject(BAD_NAME).put("id", 13); + twoProps.put("other", 1); + + assertEquals("<__I_am__fancy_____>13", + mapper.writeValueAsString(oneProp)); + assertEquals("<__I_am__fancy_____>13" + +"1", + mapper.writeValueAsString(twoProps)); + } + + // ... which also makes the WRAP_ROOT_ELEMENT_NAME / UNWRAP_ROOT_OBJECT_NODE + // pairing round-trip a root name that had to be escaped + @Test + public void testUnwrappedRootNameRoundTrip() throws Exception + { + XmlMapper mapper = mapperWith(XmlNameProcessors.newBase64Processor()); + // decodes to "$ I am ! &;" + final String DOC = "13" + +""; + + assertEquals(DOC, mapper.writeValueAsString(mapper.readTree(DOC))); + } + + private XmlMapper mapperWith(XmlNameProcessor proc) { + return mapperBuilder(XmlFactory.builder().xmlNameProcessor(proc).build()) + .enable(XmlReadFeature.WRAP_ROOT_ELEMENT_NAME) + .build(); + } } From b1e7fdae4a141a9daca52b8c815b949ce41a85f2 Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Tue, 28 Jul 2026 21:33:12 +0530 Subject: [PATCH 2/3] Add release notes for #887 --- release-notes/CREDITS | 3 +++ release-notes/VERSION | 2 ++ 2 files changed, 5 insertions(+) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index a898cf219..fce6260ce 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -168,3 +168,6 @@ Christian Beikov (@beikov) (3.3.0) * Fixed #883: Translate `XMLStreamReader` creation errors to `StreamReadException` (3.3.0) + * Fixed #887: Apply `XmlNameProcessor` to unwrapped `ObjectNode` root element + name + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index 082c59c27..3c9944956 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -18,6 +18,8 @@ Version: 3.x (for earlier see VERSION-2.x) #883: Translate `XMLStreamReader` creation errors to `StreamReadException` (restores guard lost in 3.x port; see 2.x `_createParser(byte[])` for #618) (fix by @Sahana2524) +#887: Apply `XmlNameProcessor` to unwrapped `ObjectNode` root element name + (fix by @Sahana2524) 3.2.2 (not yet released) From 07bb1c365a9e05e4b0c64ea15c512f201841ec3e Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 2 Aug 2026 16:36:36 -0700 Subject: [PATCH 3/3] Minor clean up --- .../dataformat/xml/ser/ToXmlGenerator.java | 10 ++-- .../xml/node/JsonNodeSerRootName887Test.java | 60 +++++++++++++++++++ .../xml/node/JsonNodeSerUnwrapped441Test.java | 46 -------------- 3 files changed, 65 insertions(+), 51 deletions(-) create mode 100644 src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerRootName887Test.java diff --git a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java index 79de2da64..1fde02287 100644 --- a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java +++ b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java @@ -520,10 +520,11 @@ public final boolean setNextNameIfMissing(QName name) /** * Method for running configured {@link XmlNameProcessor} over a name that comes - * from content being written (like {@code Map} keys or {@code ObjectNode} property - * names), as opposed to statically known POJO property names. + * from content being written (like {@code ObjectNode} property names), as opposed + * to statically known POJO property names. * Needed by callers that have to construct the {@link QName} themselves instead - * of going through {@link #writeName(String)}. + * of going through {@link #writeName(String)} (which applies the processor for + * names it writes). * * @since 3.3 */ @@ -608,7 +609,6 @@ public JsonGenerator writeName(String name) throws JacksonException _reportError("Can not write a property name, expecting a value"); } - String ns; // 30-Jan-2024, tatu: Surprise! if (XmlWriteFeature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures) && "xsi:type".equals(name)) { @@ -629,7 +629,7 @@ public JsonGenerator writeName(String name) throws JacksonException } } else { // Should this ever get called? - ns = (_nextName == null) ? "" : _nextName.getNamespaceURI(); + String ns = (_nextName == null) ? "" : _nextName.getNamespaceURI(); setNextName(encodeContentName(ns, name)); } return this; diff --git a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerRootName887Test.java b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerRootName887Test.java new file mode 100644 index 000000000..4d9072da0 --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerRootName887Test.java @@ -0,0 +1,60 @@ +package tools.jackson.dataformat.xml.node; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.node.ObjectNode; + +import tools.jackson.dataformat.xml.XmlFactory; +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.dataformat.xml.XmlNameProcessor; +import tools.jackson.dataformat.xml.XmlNameProcessors; +import tools.jackson.dataformat.xml.XmlReadFeature; +import tools.jackson.dataformat.xml.XmlTestUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// [dataformat-xml#887]: root name of unwrapped `ObjectNode` (see +// `XmlWriteFeature.UNWRAP_ROOT_OBJECT_NODE`) needs `XmlNameProcessor` handling +public class JsonNodeSerRootName887Test extends XmlTestUtil +{ + // The unwrapped root name comes from content, so it needs the same + // XmlNameProcessor treatment the name gets when written as a child element. + @Test + public void testUnwrappedRootNameIsProcessed() throws Exception + { + final String BAD_NAME = "$ I am ! &;"; + XmlMapper mapper = mapperWith(XmlNameProcessors.newReplacementProcessor()); + + ObjectNode oneProp = mapper.createObjectNode(); + oneProp.putObject(BAD_NAME).put("id", 13); + + ObjectNode twoProps = mapper.createObjectNode(); + twoProps.putObject(BAD_NAME).put("id", 13); + twoProps.put("other", 1); + + assertEquals("<__I_am__fancy_____>13", + mapper.writeValueAsString(oneProp)); + assertEquals("<__I_am__fancy_____>13" + +"1", + mapper.writeValueAsString(twoProps)); + } + + // ... which also makes the WRAP_ROOT_ELEMENT_NAME / UNWRAP_ROOT_OBJECT_NODE + // pairing round-trip a root name that had to be escaped + @Test + public void testUnwrappedRootNameRoundTrip() throws Exception + { + XmlMapper mapper = mapperWith(XmlNameProcessors.newBase64Processor()); + // decodes to "$ I am ! &;" + final String DOC = "13" + +""; + + assertEquals(DOC, mapper.writeValueAsString(mapper.readTree(DOC))); + } + + private XmlMapper mapperWith(XmlNameProcessor proc) { + return mapperBuilder(XmlFactory.builder().xmlNameProcessor(proc).build()) + .enable(XmlReadFeature.WRAP_ROOT_ELEMENT_NAME) + .build(); + } +} diff --git a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java index 2ca5ac734..35821214e 100644 --- a/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java +++ b/src/test/java/tools/jackson/dataformat/xml/node/JsonNodeSerUnwrapped441Test.java @@ -9,11 +9,6 @@ import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ObjectNode; -import tools.jackson.dataformat.xml.XmlFactory; -import tools.jackson.dataformat.xml.XmlMapper; -import tools.jackson.dataformat.xml.XmlNameProcessor; -import tools.jackson.dataformat.xml.XmlNameProcessors; -import tools.jackson.dataformat.xml.XmlReadFeature; import tools.jackson.dataformat.xml.XmlTestUtil; import tools.jackson.dataformat.xml.XmlWriteFeature; @@ -103,45 +98,4 @@ public void testNodeAsProperty() throws Exception XML_MAPPER.writeValueAsString(stuff)); */ } - - // The unwrapped root name comes from content, so it needs the same - // XmlNameProcessor treatment the name gets when written as a child element. - @Test - public void testUnwrappedRootNameIsProcessed() throws Exception - { - final String BAD_NAME = "$ I am ! &;"; - XmlMapper mapper = mapperWith(XmlNameProcessors.newReplacementProcessor()); - - ObjectNode oneProp = mapper.createObjectNode(); - oneProp.putObject(BAD_NAME).put("id", 13); - - ObjectNode twoProps = mapper.createObjectNode(); - twoProps.putObject(BAD_NAME).put("id", 13); - twoProps.put("other", 1); - - assertEquals("<__I_am__fancy_____>13", - mapper.writeValueAsString(oneProp)); - assertEquals("<__I_am__fancy_____>13" - +"1", - mapper.writeValueAsString(twoProps)); - } - - // ... which also makes the WRAP_ROOT_ELEMENT_NAME / UNWRAP_ROOT_OBJECT_NODE - // pairing round-trip a root name that had to be escaped - @Test - public void testUnwrappedRootNameRoundTrip() throws Exception - { - XmlMapper mapper = mapperWith(XmlNameProcessors.newBase64Processor()); - // decodes to "$ I am ! &;" - final String DOC = "13" - +""; - - assertEquals(DOC, mapper.writeValueAsString(mapper.readTree(DOC))); - } - - private XmlMapper mapperWith(XmlNameProcessor proc) { - return mapperBuilder(XmlFactory.builder().xmlNameProcessor(proc).build()) - .enable(XmlReadFeature.WRAP_ROOT_ELEMENT_NAME) - .build(); - } }