Skip to content

Commit 5063969

Browse files
authored
Fix #324: support auto-detection, conversion of xsi:type on generation (#633)
1 parent 98b67f9 commit 5063969

File tree

4 files changed

+119
-34
lines changed

4 files changed

+119
-34
lines changed

release-notes/VERSION-2.x

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Project: jackson-dataformat-xml
66

77
2.17.0 (not yet released)
88

9+
#324: Support use of `xsi:type` for polymorphic serialization
10+
(`ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE`)
11+
(requested by @philipzhaoTS)
912
#618: `ArrayIndexOutOfBoundsException` thrown for invalid ending XML string
1013
when using JDK default Stax XML parser
1114
(reported by Arthur C)

src/main/java/com/fasterxml/jackson/dataformat/xml/XmlTypeResolverBuilder.java

+18-26
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
* Custom specialization of {@link StdTypeResolverBuilder}; needed so that
2222
* type id property name can be modified as necessary to make it legal
2323
* XML element or attribute name.
24+
*<p>
25+
* NOTE: Since 2.17, property name cleansing only applied to default
26+
* names (like {@code "@class"} and {@code "@type"}) but not to explicitly
27+
* specified ones (where caller presumably knows what to do).
2428
*/
2529
public class XmlTypeResolverBuilder extends StdTypeResolverBuilder
2630
{
@@ -32,33 +36,21 @@ public XmlTypeResolverBuilder(JsonTypeInfo.Value settings) {
3236
}
3337

3438
@Override
35-
public StdTypeResolverBuilder init(JsonTypeInfo.Id idType, TypeIdResolver idRes)
36-
{
37-
super.init(idType, idRes);
38-
if (_typeProperty != null) {
39-
_typeProperty = StaxUtil.sanitizeXmlTypeName(_typeProperty);
40-
}
41-
return this;
42-
}
43-
44-
@Override
45-
public StdTypeResolverBuilder init(JsonTypeInfo.Value settings, TypeIdResolver idRes) {
46-
super.init(settings, idRes);
47-
if (_typeProperty != null) {
48-
_typeProperty = StaxUtil.sanitizeXmlTypeName(_typeProperty);
49-
}
50-
return this;
51-
}
52-
53-
@Override
54-
public StdTypeResolverBuilder typeProperty(String typeIdPropName)
55-
{
56-
// ok to have null/empty; will restore to use defaults
57-
if (typeIdPropName == null || typeIdPropName.length() == 0) {
58-
typeIdPropName = _idType.getDefaultPropertyName();
39+
protected String _propName(String propName, JsonTypeInfo.Id idType) {
40+
// 30-Jan-2024, tatu: Before 2.17 we used to indiscriminately cleanse
41+
// property name always; with 2.17+ only default ones
42+
if (propName == null || propName.isEmpty()) {
43+
propName = StaxUtil.sanitizeXmlTypeName(idType.getDefaultPropertyName());
44+
} else {
45+
// ... alas, there's... a "feature" (read: bug) in `JsonTypeInfo.Value` construction
46+
// which will automatically apply default property name if no explicit property
47+
// name specific. This means we don't really know if default is being used.
48+
// But let's assume that if "propName.equals(defaultPropName)" this is the case.
49+
if (propName.equals(idType.getDefaultPropertyName())) {
50+
propName = StaxUtil.sanitizeXmlTypeName(propName);
51+
}
5952
}
60-
_typeProperty = StaxUtil.sanitizeXmlTypeName(typeIdPropName);
61-
return this;
53+
return propName;
6254
}
6355

6456
@Override

src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java

+35-8
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ public enum Feature implements FormatFeature
9393
* @since 2.13
9494
*/
9595
UNWRAP_ROOT_OBJECT_NODE(false),
96+
97+
/**
98+
* Feature that enables automatic conversion of logical property
99+
* name {@code "xsi:type"} into matching XML name where "type"
100+
* is the local name and "xsi" prefix is bound to URI
101+
* {@link XMLConstants#W3C_XML_SCHEMA_INSTANCE_NS_URI},
102+
* and output is indicated to be done as XML Attribute.
103+
* This is mostly desirable for Polymorphic handling where it is difficult
104+
* to specify XML Namespace for type identifier
105+
*
106+
* @since 2.17
107+
*/
108+
AUTO_DETECT_XSI_TYPE(false),
96109
;
97110

98111
final boolean _defaultState;
@@ -264,6 +277,9 @@ public void initGenerator() throws IOException
264277
_xmlPrettyPrinter.writePrologLinefeed(_xmlWriter);
265278
}
266279
}
280+
if (Feature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)) {
281+
_xmlWriter.setPrefix("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
282+
}
267283
} catch (XMLStreamException e) {
268284
StaxUtil.throwAsGenerationException(e, this);
269285
}
@@ -502,12 +518,22 @@ public final void writeFieldName(String name) throws IOException
502518
if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) {
503519
_reportError("Can not write a field name, expecting a value");
504520
}
505-
// Should this ever get called?
506-
String ns = (_nextName == null) ? "" : _nextName.getNamespaceURI();
507-
_nameToEncode.namespace = ns;
508-
_nameToEncode.localPart = name;
509-
_nameProcessor.encodeName(_nameToEncode);
510-
setNextName(new QName(_nameToEncode.namespace, _nameToEncode.localPart));
521+
522+
String ns;
523+
// 30-Jan-2024, tatu: Surprise!
524+
if (Feature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)
525+
&& "xsi:type".equals(name)) {
526+
setNextName(new QName(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
527+
"type", "xsi"));
528+
setNextIsAttribute(true);
529+
} else {
530+
// Should this ever get called?
531+
ns = (_nextName == null) ? "" : _nextName.getNamespaceURI();
532+
_nameToEncode.namespace = ns;
533+
_nameToEncode.localPart = name;
534+
_nameProcessor.encodeName(_nameToEncode);
535+
setNextName(new QName(_nameToEncode.namespace, _nameToEncode.localPart));
536+
}
511537
}
512538

513539
@Override
@@ -523,9 +549,10 @@ public final void writeStringField(String fieldName, String value) throws IOExce
523549
// handling...
524550
//
525551
// See [dataformat-xml#4] for more context.
526-
552+
553+
// 30-Jan-2024, tatu: With 2.17 we may want to revisit this.
527554
/*
528-
// @since 2.9
555+
@Override
529556
public WritableTypeId writeTypePrefix(WritableTypeId typeIdDef) throws IOException
530557
{
531558
// 03-Aug-2017, tatu: Due to XML oddities, we do need to massage things
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.fasterxml.jackson.dataformat.xml.ser;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonRootName;
5+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
6+
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
7+
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
8+
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
9+
import com.fasterxml.jackson.dataformat.xml.XmlTestBase;
10+
11+
// [dataformat-xml#324]
12+
public class XsiTypeWriteTest extends XmlTestBase
13+
{
14+
@JsonRootName("Typed")
15+
static class TypeBean {
16+
@JsonProperty("xsi:type")
17+
public String typeId = "abc";
18+
}
19+
20+
@JsonRootName("Poly")
21+
@JsonTypeInfo(use = Id.SIMPLE_NAME, include = As.PROPERTY, property="xsi:type")
22+
static class PolyBean {
23+
public int value = 42;
24+
}
25+
26+
private final XmlMapper NO_XSI_MAPPER = XmlMapper.builder()
27+
.configure(ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE, false)
28+
.build();
29+
30+
private final XmlMapper XSI_ENABLED_MAPPER = XmlMapper.builder()
31+
.configure(ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE, true)
32+
.build();
33+
34+
public void testExplicitXsiTypeWriteDisabled() throws Exception
35+
{
36+
assertEquals("<Typed><xsi:type>abc</xsi:type></Typed>",
37+
NO_XSI_MAPPER.writeValueAsString(new TypeBean()));
38+
}
39+
40+
public void testExplicitXsiTypeWriteEnabled() throws Exception
41+
{
42+
assertEquals(
43+
a2q("<Typed xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:type='abc'/>"),
44+
a2q(XSI_ENABLED_MAPPER.writeValueAsString(new TypeBean())));
45+
}
46+
47+
public void testXsiTypeAsTypeIdWriteDisabled() throws Exception
48+
{
49+
// not legal XML but with explicitly specified name is what caller wants
50+
// (note: not 100% sure how xsi:type is written as attribute)
51+
assertEquals(
52+
a2q("<Poly xsi:type='PolyBean'><value>42</value></Poly>"),
53+
a2q(NO_XSI_MAPPER.writeValueAsString(new PolyBean())));
54+
}
55+
56+
public void testXsiTypeAsTypeIdWriteEnabled() throws Exception
57+
{
58+
assertEquals(
59+
a2q("<Poly xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:type='PolyBean'>"
60+
+"<value>42</value></Poly>"),
61+
a2q(XSI_ENABLED_MAPPER.writeValueAsString(new PolyBean())));
62+
}
63+
}

0 commit comments

Comments
 (0)