Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ Christian Beikov (@beikov)
`XmlBeanSerializerBase` (wrong attribute/text/CDATA handling after
`@JsonIgnoreProperties`)
(3.3.0)
* Fixed #909: Clear forced `xsi:type` attribute state in
`ToXmlGenerator.writeName()` (`Map`/`JsonNode` key `xsi:type` leaked
attribute mode onto following siblings)
(3.3.0)
* Fixed #911: Verify Stax factory type before instantiating in
`XmlFactory.readResolve()`
(3.1.7)
Expand Down
3 changes: 3 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ Version: 3.x (for earlier see VERSION-2.x)
#907: Recompute XML metadata for filtered properties in `XmlBeanSerializerBase`
(wrong attribute/text/CDATA handling after `@JsonIgnoreProperties`)
(fix by @Sahana2524)
#909: Clear forced `xsi:type` attribute state in `ToXmlGenerator.writeName()`
(`Map`/`JsonNode` key `xsi:type` leaked attribute mode onto following siblings)
(fix by @Sahana2524)

3.2.3 (21-Sep-2026)

Expand Down
29 changes: 29 additions & 0 deletions src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ public class ToXmlGenerator
*/
protected boolean _nextIsCData = false;

/**
* Marker set by {@link #writeName(String)} when it forces attribute mode
* and the XSI namespace onto the next value to emit a synthetic
* {@code xsi:type} attribute. Bean serializers clear it by assigning the
* following name via {@link #setNextName}; for content-driven names
* (Map / JsonNode / {@code @JsonAnyGetter}) nothing else does, so the next
* {@link #writeName} clears it to keep the forced state from leaking onto a
* following sibling.
*/
protected boolean _nextIsXsiType = false;

/**
* To support proper serialization of arrays it is necessary to keep
* stack of element names, so that we can "revert" to earlier
Expand Down Expand Up @@ -502,6 +513,8 @@ public void setNextIsCData(boolean isCData)
public final void setNextName(QName name)
{
_nextName = name;
// Caller is taking over naming, so drop any pending xsi:type forcing
_nextIsXsiType = false;
}

/**
Expand Down Expand Up @@ -610,12 +623,28 @@ public JsonGenerator writeName(String name) throws JacksonException
_reportError("Can not write a property name, expecting a value");
}

// A preceding synthetic "xsi:type" name forces attribute mode and the XSI
// namespace onto _nextName so the type-id value can be written as an
// attribute. Bean serializers reset that by assigning the next name via
// setNextName(); content-driven names (Map/JsonNode/@JsonAnyGetter) do not,
// so clear it here. Otherwise the following sibling inherits attribute-ness
// and the XSI namespace, producing xsi:-prefixed attributes (or a duplicate
// xsi:type) that this module can no longer read back.
// Revert to enclosing element name (if any) so the following sibling inherits
// its namespace, same as it would without the preceding "xsi:type".
if (_nextIsXsiType) {
_nextIsXsiType = false;
_nextIsAttribute = false;
_nextName = _elementNameStack.peekLast();
}

// 30-Jan-2024, tatu: Surprise!
if (XmlWriteFeature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)
&& "xsi:type".equals(name)) {
setNextName(new QName(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
"type", "xsi"));
setNextIsAttribute(true);
_nextIsXsiType = true;
} else if (name.equals(_cfgNameForTextElement)) {
// [dataformat-xml#629]: Name matching the "unnamed text property" marker
// (FromXmlParser.DEFAULT_UNNAMED_TEXT_PROPERTY, default "") represents
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package tools.jackson.dataformat.xml.ser;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

import tools.jackson.databind.node.ObjectNode;
import tools.jackson.dataformat.xml.XmlMapper;
import tools.jackson.dataformat.xml.XmlTestUtil;
import tools.jackson.dataformat.xml.XmlWriteFeature;
import tools.jackson.dataformat.xml.annotation.JacksonXmlProperty;

import static org.junit.jupiter.api.Assertions.assertEquals;

Expand All @@ -27,6 +33,11 @@ static class PolyBean {
public int value = 42;
}

static class NsMapBean {
@JacksonXmlProperty(namespace = "urn:x")
public Map<String, Object> map = new LinkedHashMap<>();
}

private final XmlMapper NO_XSI_MAPPER = XmlMapper.builder()
.configure(XmlWriteFeature.AUTO_DETECT_XSI_TYPE, false)
.build();
Expand Down Expand Up @@ -68,4 +79,80 @@ public void testXsiTypeAsTypeIdWriteEnabled() throws Exception
+"<value>42</value></Poly>"),
a2q(XSI_ENABLED_MAPPER.writeValueAsString(new PolyBean())));
}

// Content-driven names (Map/JsonNode) that happen to include an "xsi:type"
// key must not leave following siblings in attribute mode / the XSI namespace.
// Before the fix the forced state leaked, so siblings became xsi:-prefixed
// attributes -- and a colliding "type" key produced a duplicate xsi:type
// attribute, i.e. XML this module could no longer read back.
@Test
public void testXsiTypeKeyDoesNotLeakOntoSibling() throws Exception
{
XmlMapper mapper = newMapper();
ObjectNode tree = mapper.createObjectNode();
tree.put("xsi:type", "A");
tree.put("type", "B");
String xml = mapper.writeValueAsString(tree);
assertEquals(
a2q("<ObjectNode xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"
+" xsi:type='A'><type>B</type></ObjectNode>"),
a2q(xml));
// and the emitted document must round-trip through the same module
assertEquals(tree, mapper.readTree(xml));
}

@Test
public void testXsiTypeKeyFollowedByNilKey() throws Exception
{
XmlMapper mapper = newMapper();
ObjectNode tree = mapper.createObjectNode();
tree.put("xsi:type", "T");
tree.put("nil", "true");
String xml = mapper.writeValueAsString(tree);
assertEquals(
a2q("<ObjectNode xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"
+" xsi:type='T'><nil>true</nil></ObjectNode>"),
a2q(xml));
// previously the leaked attribute mode emitted xsi:nil='true', collapsing
// the whole document to null on read
assertEquals(tree, mapper.readTree(xml));
}

// Same for a plain Map; repeated (List-valued) sibling used to become
// duplicate "xsi:list" attributes
@Test
public void testXsiTypeMapKeyFollowedByList() throws Exception
{
XmlMapper mapper = newMapper();
Map<String, Object> map = new LinkedHashMap<>();
map.put("xsi:type", "T");
map.put("list", List.of(1, 2));
assertEquals(
a2q("<LinkedHashMap xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"
+" xsi:type='T'><list>1</list><list>2</list></LinkedHashMap>"),
a2q(mapper.writeValueAsString(map)));
}

// Sibling following "xsi:type" must still inherit enclosing element's namespace,
// same as it would without the "xsi:type" key
@Test
public void testXsiTypeMapKeyKeepsEnclosingNamespace() throws Exception
{
XmlMapper mapper = newMapper();
NsMapBean bean = new NsMapBean();
bean.map.put("b", 2);
assertEquals(
a2q("<NsMapBean><wstxns1:map xmlns:wstxns1='urn:x'>"
+"<wstxns1:b>2</wstxns1:b></wstxns1:map></NsMapBean>"),
a2q(mapper.writeValueAsString(bean)));

bean = new NsMapBean();
bean.map.put("xsi:type", "T");
bean.map.put("b", 2);
assertEquals(
a2q("<NsMapBean><wstxns1:map xmlns:wstxns1='urn:x'"
+" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:type='T'>"
+"<wstxns1:b>2</wstxns1:b></wstxns1:map></NsMapBean>"),
a2q(mapper.writeValueAsString(bean)));
}
}
Loading