Skip to content

Commit

Permalink
Fix #324: support auto-detection, conversion of xsi:type on generat…
Browse files Browse the repository at this point in the history
…ion (#633)
  • Loading branch information
cowtowncoder authored Jan 31, 2024
1 parent 98b67f9 commit 5063969
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 34 deletions.
3 changes: 3 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Project: jackson-dataformat-xml

2.17.0 (not yet released)

#324: Support use of `xsi:type` for polymorphic serialization
(`ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE`)
(requested by @philipzhaoTS)
#618: `ArrayIndexOutOfBoundsException` thrown for invalid ending XML string
when using JDK default Stax XML parser
(reported by Arthur C)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
* Custom specialization of {@link StdTypeResolverBuilder}; needed so that
* type id property name can be modified as necessary to make it legal
* XML element or attribute name.
*<p>
* NOTE: Since 2.17, property name cleansing only applied to default
* names (like {@code "@class"} and {@code "@type"}) but not to explicitly
* specified ones (where caller presumably knows what to do).
*/
public class XmlTypeResolverBuilder extends StdTypeResolverBuilder
{
Expand All @@ -32,33 +36,21 @@ public XmlTypeResolverBuilder(JsonTypeInfo.Value settings) {
}

@Override
public StdTypeResolverBuilder init(JsonTypeInfo.Id idType, TypeIdResolver idRes)
{
super.init(idType, idRes);
if (_typeProperty != null) {
_typeProperty = StaxUtil.sanitizeXmlTypeName(_typeProperty);
}
return this;
}

@Override
public StdTypeResolverBuilder init(JsonTypeInfo.Value settings, TypeIdResolver idRes) {
super.init(settings, idRes);
if (_typeProperty != null) {
_typeProperty = StaxUtil.sanitizeXmlTypeName(_typeProperty);
}
return this;
}

@Override
public StdTypeResolverBuilder typeProperty(String typeIdPropName)
{
// ok to have null/empty; will restore to use defaults
if (typeIdPropName == null || typeIdPropName.length() == 0) {
typeIdPropName = _idType.getDefaultPropertyName();
protected String _propName(String propName, JsonTypeInfo.Id idType) {
// 30-Jan-2024, tatu: Before 2.17 we used to indiscriminately cleanse
// property name always; with 2.17+ only default ones
if (propName == null || propName.isEmpty()) {
propName = StaxUtil.sanitizeXmlTypeName(idType.getDefaultPropertyName());
} else {
// ... alas, there's... a "feature" (read: bug) in `JsonTypeInfo.Value` construction
// which will automatically apply default property name if no explicit property
// name specific. This means we don't really know if default is being used.
// But let's assume that if "propName.equals(defaultPropName)" this is the case.
if (propName.equals(idType.getDefaultPropertyName())) {
propName = StaxUtil.sanitizeXmlTypeName(propName);
}
}
_typeProperty = StaxUtil.sanitizeXmlTypeName(typeIdPropName);
return this;
return propName;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ public enum Feature implements FormatFeature
* @since 2.13
*/
UNWRAP_ROOT_OBJECT_NODE(false),

/**
* Feature that enables automatic conversion of logical property
* name {@code "xsi:type"} into matching XML name where "type"
* is the local name and "xsi" prefix is bound to URI
* {@link XMLConstants#W3C_XML_SCHEMA_INSTANCE_NS_URI},
* and output is indicated to be done as XML Attribute.
* This is mostly desirable for Polymorphic handling where it is difficult
* to specify XML Namespace for type identifier
*
* @since 2.17
*/
AUTO_DETECT_XSI_TYPE(false),
;

final boolean _defaultState;
Expand Down Expand Up @@ -264,6 +277,9 @@ public void initGenerator() throws IOException
_xmlPrettyPrinter.writePrologLinefeed(_xmlWriter);
}
}
if (Feature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)) {
_xmlWriter.setPrefix("xsi", XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
}
} catch (XMLStreamException e) {
StaxUtil.throwAsGenerationException(e, this);
}
Expand Down Expand Up @@ -502,12 +518,22 @@ public final void writeFieldName(String name) throws IOException
if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) {
_reportError("Can not write a field name, expecting a value");
}
// Should this ever get called?
String ns = (_nextName == null) ? "" : _nextName.getNamespaceURI();
_nameToEncode.namespace = ns;
_nameToEncode.localPart = name;
_nameProcessor.encodeName(_nameToEncode);
setNextName(new QName(_nameToEncode.namespace, _nameToEncode.localPart));

String ns;
// 30-Jan-2024, tatu: Surprise!
if (Feature.AUTO_DETECT_XSI_TYPE.enabledIn(_formatFeatures)
&& "xsi:type".equals(name)) {
setNextName(new QName(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
"type", "xsi"));
setNextIsAttribute(true);
} 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));
}
}

@Override
Expand All @@ -523,9 +549,10 @@ public final void writeStringField(String fieldName, String value) throws IOExce
// handling...
//
// See [dataformat-xml#4] for more context.


// 30-Jan-2024, tatu: With 2.17 we may want to revisit this.
/*
// @since 2.9
@Override
public WritableTypeId writeTypePrefix(WritableTypeId typeIdDef) throws IOException
{
// 03-Aug-2017, tatu: Due to XML oddities, we do need to massage things
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.fasterxml.jackson.dataformat.xml.ser;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.XmlTestBase;

// [dataformat-xml#324]
public class XsiTypeWriteTest extends XmlTestBase
{
@JsonRootName("Typed")
static class TypeBean {
@JsonProperty("xsi:type")
public String typeId = "abc";
}

@JsonRootName("Poly")
@JsonTypeInfo(use = Id.SIMPLE_NAME, include = As.PROPERTY, property="xsi:type")
static class PolyBean {
public int value = 42;
}

private final XmlMapper NO_XSI_MAPPER = XmlMapper.builder()
.configure(ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE, false)
.build();

private final XmlMapper XSI_ENABLED_MAPPER = XmlMapper.builder()
.configure(ToXmlGenerator.Feature.AUTO_DETECT_XSI_TYPE, true)
.build();

public void testExplicitXsiTypeWriteDisabled() throws Exception
{
assertEquals("<Typed><xsi:type>abc</xsi:type></Typed>",
NO_XSI_MAPPER.writeValueAsString(new TypeBean()));
}

public void testExplicitXsiTypeWriteEnabled() throws Exception
{
assertEquals(
a2q("<Typed xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:type='abc'/>"),
a2q(XSI_ENABLED_MAPPER.writeValueAsString(new TypeBean())));
}

public void testXsiTypeAsTypeIdWriteDisabled() throws Exception
{
// not legal XML but with explicitly specified name is what caller wants
// (note: not 100% sure how xsi:type is written as attribute)
assertEquals(
a2q("<Poly xsi:type='PolyBean'><value>42</value></Poly>"),
a2q(NO_XSI_MAPPER.writeValueAsString(new PolyBean())));
}

public void testXsiTypeAsTypeIdWriteEnabled() throws Exception
{
assertEquals(
a2q("<Poly xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:type='PolyBean'>"
+"<value>42</value></Poly>"),
a2q(XSI_ENABLED_MAPPER.writeValueAsString(new PolyBean())));
}
}

0 comments on commit 5063969

Please sign in to comment.