Skip to content

Commit 6e52bc0

Browse files
authored
Fix #8: handle List wrapping even prop declared as Object (#809)
1 parent 0089f3d commit 6e52bc0

File tree

5 files changed

+94
-28
lines changed

5 files changed

+94
-28
lines changed

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Version: 3.x (for earlier see VERSION-2.x)
77

88
3.2.0 (not yet released)
99

10+
#8: Serialization of `List` incorrect if property declared as plain `java.lang.Object`
11+
(fix by @cowtowncoder, w/ Claude code)
1012
#802: Serialization with Polymorphisme and `EXTERNAL_PROPERTY` = duplicate property
1113
(reported by @ Adrien-dev25 )
1214
(fix by @cowtowncoder, w/ Claude code)

src/main/java/tools/jackson/dataformat/xml/ser/XmlBeanPropertyWriter.java

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@
99

1010
/**
1111
* Property writer sub-class used for handling element wrapping needed for serializing
12-
* collection (array, Collection; possibly Map) types.
12+
* collection (array, Collection; not Map) types.
1313
*/
1414
public class XmlBeanPropertyWriter
1515
extends BeanPropertyWriter
1616
{
17-
private static final long serialVersionUID = 1L;
18-
1917
/*
2018
/**********************************************************************
2119
/* Config settings
@@ -32,24 +30,60 @@ public class XmlBeanPropertyWriter
3230
*/
3331
protected final QName _wrappedQName;
3432

33+
/**
34+
* Whether wrapping should be checked dynamically based on the runtime value type.
35+
* When {@code true}, wrapping is only applied if the runtime value is actually
36+
* a Collection, Iterable, or array. Used for properties declared as {@code Object}
37+
* that may or may not contain a collection at runtime.
38+
*
39+
* @since 3.2
40+
*/
41+
protected final boolean _dynamicWrapping;
42+
3543
/*
3644
/**********************************************************************
3745
/* Life-cycle: construction, configuration
3846
/**********************************************************************
3947
*/
4048

49+
/**
50+
* @deprecated Since 3.2
51+
*/
52+
@Deprecated
4153
public XmlBeanPropertyWriter(BeanPropertyWriter wrapped,
4254
PropertyName wrapperName, PropertyName wrappedName) {
43-
this(wrapped, wrapperName, wrappedName, null);
55+
this(wrapped, wrapperName, wrappedName, null, false);
4456
}
4557

58+
/**
59+
* @deprecated Since 3.2
60+
*/
61+
@Deprecated
4662
public XmlBeanPropertyWriter(BeanPropertyWriter wrapped,
4763
PropertyName wrapperName, PropertyName wrappedName,
4864
ValueSerializer<Object> serializer)
65+
{
66+
this(wrapped, wrapperName, wrappedName, serializer, false);
67+
}
68+
69+
/**
70+
* @since 3.2
71+
*/
72+
public XmlBeanPropertyWriter(BeanPropertyWriter wrapped,
73+
PropertyName wrapperName, PropertyName wrappedName,
74+
boolean dynamicWrapping)
75+
{
76+
this(wrapped, wrapperName, wrappedName, null, dynamicWrapping);
77+
}
78+
79+
private XmlBeanPropertyWriter(BeanPropertyWriter wrapped,
80+
PropertyName wrapperName, PropertyName wrappedName,
81+
ValueSerializer<Object> serializer, boolean dynamicWrapping)
4982
{
5083
super(wrapped);
5184
_wrapperQName = _qname(wrapperName);
5285
_wrappedQName = _qname(wrappedName);
86+
_dynamicWrapping = dynamicWrapping;
5387

5488
if (serializer != null) {
5589
assignSerializer(serializer);
@@ -64,7 +98,7 @@ private QName _qname(PropertyName n)
6498
}
6599
return new QName(ns, n.getSimpleName());
66100
}
67-
101+
68102
/*
69103
/**********************************************************************
70104
/* Overridden methods
@@ -81,10 +115,17 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte
81115
{
82116
Object value = get(bean);
83117

118+
// [dataformat-xml#8]: For dynamic wrapping (Object-typed properties),
119+
// check runtime type and delegate to standard handling if not a collection
120+
if (_dynamicWrapping && (value == null || !_isIndexedValue(value))) {
121+
super.serializeAsProperty(bean, g, ctxt);
122+
return;
123+
}
124+
84125
/* 13-Feb-2014, tatu: As per [#103], default handling does not really
85126
* work here. Rather, we need just a wrapping and should NOT call
86127
* null handler, as it does not know what to do...
87-
*
128+
*
88129
* Question, however, is what should it be serialized as. We have two main
89130
* choices; equivalent empty List, and "nothing" (missing). Let's start with
90131
* empty List? But producing missing entry is non-trivial...
@@ -102,7 +143,7 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte
102143
}
103144
*/
104145
// but for missing thing, well, just output nothing
105-
146+
106147
return;
107148
}
108149

@@ -150,4 +191,14 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte
150191
xmlGen.finishWrappedValue(_wrapperQName, _wrappedQName);
151192
}
152193
}
194+
195+
/**
196+
* Check if the runtime value is a Collection, array, or Iterable
197+
* (i.e. something that should get wrapper element handling).
198+
*/
199+
private static boolean _isIndexedValue(Object value) {
200+
return (value instanceof java.util.Collection<?>)
201+
|| (value instanceof Iterable<?>)
202+
|| value.getClass().isArray();
203+
}
153204
}

src/main/java/tools/jackson/dataformat/xml/ser/XmlBeanSerializerModifier.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,22 @@ public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
4848
bpw.setInternalSetting(XmlBeanSerializerBase.KEY_XML_INFO,
4949
new XmlInfo(isAttribute, ns, isText, isCData));
5050

51-
// Actually: if we have a Collection type, easiest place to add wrapping would be here...
52-
// or: let's also allow wrapping of "untyped" (Object): assuming it is a dynamically
53-
// typed Collection...
54-
if (!TypeUtil.isIndexedType(bpw.getType())) {
51+
// If we have a Collection/array type, the easiest place to add wrapping is here.
52+
// [dataformat-xml#8]: also allow wrapping of "untyped" (Object): assuming it may
53+
// be a dynamically typed Collection at runtime. Use dynamic wrapping so that
54+
// wrapping is only applied when runtime value is actually a Collection.
55+
final JavaType propType = bpw.getType();
56+
final boolean dynamicWrapping;
57+
58+
if (TypeUtil.isIndexedType(propType)) {
59+
dynamicWrapping = false;
60+
} else if (propType.isJavaLangObject()
61+
// [dataformat-xml#8]: for Object-typed properties, only wrap standard
62+
// BeanPropertyWriters; skip virtual properties (e.g. @JsonAppend)
63+
// and other custom subclasses whose get() won't survive wrapping
64+
&& bpw.getClass() == BeanPropertyWriter.class) {
65+
dynamicWrapping = true;
66+
} else {
5567
continue;
5668
}
5769
PropertyName wrappedName = PropertyName.construct(bpw.getName(), ns);
@@ -66,7 +78,10 @@ public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
6678
if (localName == null || localName.length() == 0) {
6779
wrapperName = wrappedName;
6880
}
69-
beanProperties.set(i, new XmlBeanPropertyWriter(bpw, wrapperName, wrappedName));
81+
// [dataformat-xml#8]: for Object-typed properties, use dynamic wrapping
82+
// that checks at runtime if the value is actually a Collection
83+
beanProperties.set(i,
84+
new XmlBeanPropertyWriter(bpw, wrapperName, wrappedName, dynamicWrapping));
7085
}
7186
return beanProperties;
7287
}

src/main/java/tools/jackson/dataformat/xml/util/TypeUtil.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ public class TypeUtil
1010
*/
1111
public static boolean isIndexedType(JavaType type)
1212
{
13-
Class<?> cls = type.getRawClass();
1413
// 25-Mar-2024, tatu [dataformat-xml#646]: Need to support Iterable too
15-
if (type.isContainerType() || type.isIterationType() || cls == Iterable.class) {
16-
// One special case; byte[] will be serialized as base64-encoded String, not real array, so:
17-
// (actually, ditto for char[]; thought to be a String)
18-
if (cls == byte[].class || cls == char[].class) {
19-
return false;
20-
}
14+
if (type.isContainerType() || type.isIterationType() || type.hasRawClass(Iterable.class)) {
2115
// Also, should not add wrapping for Maps
2216
// [dataformat-xml#220]: nor map-like (Scala Map) types
2317
if (type.isMapLikeType()) {
2418
return false;
2519
}
20+
if (type.isArrayType()) {
21+
// Other special cases; byte[] will be serialized as base64-encoded String,
22+
// not real array, so...
23+
// (actually, ditto for char[]; thought to be a String)
24+
if (type.hasRawClass(byte[].class) || type.hasRawClass(char[].class)) {
25+
return false;
26+
}
27+
}
2628
return true;
2729
}
2830
return false;

src/test/java/tools/jackson/dataformat/xml/tofix/UntypedListSerialization8Test.java renamed to src/test/java/tools/jackson/dataformat/xml/ser/UntypedListSerialization8Test.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package tools.jackson.dataformat.xml.tofix;
1+
package tools.jackson.dataformat.xml.ser;
22

33
import java.io.IOException;
44
import java.util.ArrayList;
@@ -10,17 +10,17 @@
1010

1111
import tools.jackson.dataformat.xml.XmlMapper;
1212
import tools.jackson.dataformat.xml.XmlTestUtil;
13-
import tools.jackson.dataformat.xml.testutil.failure.JacksonTestFailureExpected;
1413

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

16+
// [dataformat-xml#8]: Serialization of List incorrect if property declared as Object
1717
public class UntypedListSerialization8Test extends XmlTestUtil
1818
{
1919
@JsonRootName("L")
2020
static class UntypedListBean
2121
{
2222
public final Object list;
23-
23+
2424
public UntypedListBean() {
2525
ArrayList<String> l= new ArrayList<String>();
2626
l.add("first");
@@ -33,7 +33,7 @@ public UntypedListBean() {
3333
static class TypedListBean
3434
{
3535
public final List<String> list;
36-
36+
3737
public TypedListBean() {
3838
ArrayList<String> l= new ArrayList<String>();
3939
l.add("first");
@@ -50,11 +50,7 @@ public TypedListBean() {
5050

5151
private final XmlMapper MAPPER = newMapper();
5252

53-
/*
54-
* For [dataformat-xml#8] -- Will not use wrapping, if type is not statically known
55-
* to be a Collection
56-
*/
57-
@JacksonTestFailureExpected
53+
// [dataformat-xml#8]
5854
@Test
5955
public void testListAsObject() throws IOException
6056
{

0 commit comments

Comments
 (0)