Skip to content

Commit 3ac186c

Browse files
jkosh44cowtowncoder
authored andcommitted
Allow registering the same subtype multiple times
Before this commit NamedType hashes only on it's class and not on the name. This only allowed you to register a class once using ObjectMapper.registerSubtypes(NamedType... types). This commit now uses name field to hash NamedTypes. This successfully allows you to deserialize objects of the same type but different name. Serializing objects of the same type but different name (for example fields of a POJO) still has some issues. 1. SerializerProvider caches Serializers based on class and doesn't take name into account. 2. TypeIdResolver has no method to resolve an id that takes name into account. Therefore when resolving an id we only have the value and type. 3. TypeNameIdResolver stores type and id information as a Map<String, String> that maps type to id, ignoring name Fixes #2515
1 parent 6042bcf commit 3ac186c

File tree

3 files changed

+81
-17
lines changed

3 files changed

+81
-17
lines changed

src/main/java/com/fasterxml/jackson/databind/jsontype/NamedType.java

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.fasterxml.jackson.databind.jsontype;
22

3+
import java.util.Objects;
4+
35
/**
46
* Simple container class for types with optional logical name, used
57
* as external identifier
@@ -14,10 +16,10 @@ public final class NamedType implements java.io.Serializable
1416
protected String _name;
1517

1618
public NamedType(Class<?> c) { this(c, null); }
17-
19+
1820
public NamedType(Class<?> c, String name) {
1921
_class = c;
20-
_hashCode = c.getName().hashCode();
22+
_hashCode = (c.getName() + (name != null ? name : "")).hashCode();
2123
setName(name);
2224
}
2325

@@ -28,14 +30,16 @@ public NamedType(Class<?> c, String name) {
2830
public boolean hasName() { return _name != null; }
2931

3032
/**
31-
* Equality is defined based on class only, not on name
33+
* Equality is defined based on class and name
3234
*/
3335
@Override
3436
public boolean equals(Object o) {
3537
if (o == this) return true;
3638
if (o == null) return false;
3739
if (o.getClass() != getClass()) return false;
38-
return _class == ((NamedType) o)._class;
40+
NamedType other = (NamedType)o;
41+
if (!Objects.equals(_name, other._name)) return false;
42+
return _class == other._class;
3943
}
4044

4145
@Override

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdSubtypeResolver.java

+10-5
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public Collection<NamedType> collectAndResolveSubtypesByClass(MapperConfig<?> co
112112
AnnotatedClass type)
113113
{
114114
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
115-
HashMap<NamedType, NamedType> subtypes = new HashMap<NamedType, NamedType>();
115+
HashMap<NamedType, NamedType> subtypes = new HashMap<>();
116+
116117
// then consider registered subtypes (which have precedence over annotations)
117118
if (_registeredSubtypes != null) {
118119
Class<?> rawBase = type.getRawType();
@@ -223,19 +224,23 @@ protected void _collectAndResolve(AnnotatedClass annotatedType, NamedType namedT
223224
}
224225
}
225226

227+
//For Serialization we only want to return a single NamedType per class so it's
228+
//unambiguous what name we use.
229+
NamedType typeOnlyNamedType = new NamedType(namedType.getType());
230+
226231
// First things first: is base type itself included?
227-
if (collectedSubtypes.containsKey(namedType)) {
232+
if (collectedSubtypes.containsKey(typeOnlyNamedType)) {
228233
// if so, no recursion; however, may need to update name?
229234
if (namedType.hasName()) {
230-
NamedType prev = collectedSubtypes.get(namedType);
235+
NamedType prev = collectedSubtypes.get(typeOnlyNamedType);
231236
if (!prev.hasName()) {
232-
collectedSubtypes.put(namedType, namedType);
237+
collectedSubtypes.put(typeOnlyNamedType, namedType);
233238
}
234239
}
235240
return;
236241
}
237242
// if it wasn't, add and check subtypes recursively
238-
collectedSubtypes.put(namedType, namedType);
243+
collectedSubtypes.put(typeOnlyNamedType, namedType);
239244
Collection<NamedType> st = ai.findSubtypes(annotatedType);
240245
if (st != null && !st.isEmpty()) {
241246
for (NamedType subtype : st) {

src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypes.java

+63-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.util.*;
66

7+
import com.fasterxml.jackson.annotation.JsonProperty;
78
import com.fasterxml.jackson.annotation.JsonSubTypes;
89
import com.fasterxml.jackson.annotation.JsonTypeInfo;
910
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -38,7 +39,7 @@ static class SubD extends SuperType {
3839
// "Empty" bean
3940
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME)
4041
static abstract class BaseBean { }
41-
42+
4243
static class EmptyBean extends BaseBean { }
4344

4445
static class EmptyNonFinal { }
@@ -49,7 +50,7 @@ static class PropertyBean
4950
{
5051
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME)
5152
public SuperType value;
52-
53+
5354
public PropertyBean() { this(null); }
5455
public PropertyBean(SuperType v) { value = v; }
5556
}
@@ -70,6 +71,28 @@ static class DefaultImpl505 extends SuperTypeWithoutDefault {
7071
public int a;
7172
}
7273

74+
static class Sub extends SuperTypeWithoutDefault {
75+
public int a;
76+
77+
public Sub(){}
78+
public Sub(int a) {
79+
this.a = a;
80+
}
81+
}
82+
83+
static class POJOWrapper {
84+
@JsonProperty
85+
Sub sub1;
86+
@JsonProperty
87+
Sub sub2;
88+
89+
public POJOWrapper(){}
90+
public POJOWrapper(Sub sub1, Sub sub2) {
91+
this.sub1 = sub1;
92+
this.sub2 = sub2;
93+
}
94+
}
95+
7396
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.PROPERTY, property="type")
7497
@JsonSubTypes({ @JsonSubTypes.Type(ImplX.class),
7598
@JsonSubTypes.Type(ImplY.class) })
@@ -118,7 +141,7 @@ static class Issue1125Wrapper {
118141
public Issue1125Wrapper() { }
119142
public Issue1125Wrapper(Base1125 v) { value = v; }
120143
}
121-
144+
122145
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, defaultImpl=Default1125.class)
123146
@JsonSubTypes({ @JsonSubTypes.Type(Interm1125.class) })
124147
static class Base1125 {
@@ -204,7 +227,7 @@ public void testSubtypesViaModule() throws Exception
204227
result = mapper.readValue(json, PropertyBean.class);
205228
assertSame(SubC.class, result.value.getClass());
206229
}
207-
230+
208231
public void testSerialization() throws Exception
209232
{
210233
// serialization can detect type name ok without anything extra:
@@ -217,7 +240,17 @@ public void testSerialization() throws Exception
217240
assertEquals("{\"@type\":\"typeB\",\"b\":1}", mapper.writeValueAsString(bean));
218241

219242
// and default name ought to be simple class name; with context
220-
assertEquals("{\"@type\":\"TestSubtypes$SubD\",\"d\":0}", mapper.writeValueAsString(new SubD()));
243+
assertEquals("{\"@type\":\"TestSubtypes$SubD\",\"d\":0}", mapper.writeValueAsString(new SubD()));
244+
}
245+
246+
public void testSerializationWithDuplicateRegisteredSubtypes() throws Exception {
247+
ObjectMapper mapper = new ObjectMapper();
248+
mapper.registerSubtypes(new NamedType(Sub.class, "sub1"));
249+
mapper.registerSubtypes(new NamedType(Sub.class, "sub2"));
250+
251+
// the first registered type name is used for serialization
252+
Sub sub = new Sub(15);
253+
assertEquals("{\"#type\":\"sub1\",\"a\":15}", mapper.writeValueAsString(sub));
221254
}
222255

223256
public void testDeserializationNonNamed() throws Exception
@@ -247,6 +280,28 @@ public void testDeserializatioNamed() throws Exception
247280
assertEquals(-4, ((SubD) bean).d);
248281
}
249282

283+
public void testDeserializationWithDuplicateRegisteredSubtypes()
284+
throws Exception {
285+
ObjectMapper mapper = new ObjectMapper();
286+
287+
// We can register the same class with different names
288+
mapper.registerSubtypes(new NamedType(Sub.class, "sub1"));
289+
mapper.registerSubtypes(new NamedType(Sub.class, "sub2"));
290+
291+
// fields of a POJO will be deserialized correctly according to their field name
292+
POJOWrapper pojoWrapper = mapper.readValue("{\"sub1\":{\"#type\":\"sub1\",\"a\":10},\"sub2\":{\"#type\":\"sub2\",\"a\":50}}", POJOWrapper.class);
293+
assertEquals(10, pojoWrapper.sub1.a);
294+
assertEquals(50, pojoWrapper.sub2.a);
295+
296+
// Instances of the same object can be deserialized with multiple names
297+
SuperTypeWithoutDefault sub1 = mapper.readValue("{\"#type\":\"sub1\", \"a\":20}", SuperTypeWithoutDefault.class);
298+
assertSame(Sub.class, sub1.getClass());
299+
assertEquals(20, ((Sub) sub1).a);
300+
SuperTypeWithoutDefault sub2 = mapper.readValue("{\"#type\":\"sub2\", \"a\":30}", SuperTypeWithoutDefault.class);
301+
assertSame(Sub.class, sub2.getClass());
302+
assertEquals(30, ((Sub) sub2).a);
303+
}
304+
250305
// Trying to reproduce [JACKSON-366]
251306
public void testEmptyBean() throws Exception
252307
{
@@ -295,7 +350,7 @@ public void testDefaultImpl() throws Exception
295350
public void testDefaultImplViaModule() throws Exception
296351
{
297352
final String JSON = "{\"a\":123}";
298-
353+
299354
// first: without registration etc, epic fail:
300355
try {
301356
MAPPER.readValue(JSON, SuperTypeWithoutDefault.class);
@@ -317,7 +372,7 @@ public void testDefaultImplViaModule() throws Exception
317372
bean = mapper.readValue("{\"#type\":\"foobar\"}", SuperTypeWithoutDefault.class);
318373
assertEquals(DefaultImpl505.class, bean.getClass());
319374
assertEquals(0, ((DefaultImpl505) bean).a);
320-
375+
321376
}
322377

323378
public void testErrorMessage() throws Exception {
@@ -361,7 +416,7 @@ public void testSubclassLimits() throws Exception
361416
public void testIssue1125NonDefault() throws Exception
362417
{
363418
String json = MAPPER.writeValueAsString(new Issue1125Wrapper(new Impl1125(1, 2, 3)));
364-
419+
365420
Issue1125Wrapper result = MAPPER.readValue(json, Issue1125Wrapper.class);
366421
assertNotNull(result.value);
367422
assertEquals(Impl1125.class, result.value.getClass());

0 commit comments

Comments
 (0)