Skip to content

Commit c06e132

Browse files
committed
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 FasterXML#2515
1 parent c12f20d commit c06e132

File tree

3 files changed

+67
-16
lines changed

3 files changed

+67
-16
lines changed

Diff for: src/main/java/com/fasterxml/jackson/databind/jsontype/NamedType.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ public final class NamedType implements java.io.Serializable
1414
protected String _name;
1515

1616
public NamedType(Class<?> c) { this(c, null); }
17-
17+
1818
public NamedType(Class<?> c, String name) {
1919
_class = c;
20-
_hashCode = c.getName().hashCode();
20+
_hashCode = (c.getName() + (name != null ? name : "")).hashCode();
2121
setName(name);
2222
}
2323

Diff for: 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) {

Diff for: src/test/java/com/fasterxml/jackson/databind/jsontype/TestSubtypes.java

+55-9
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:
@@ -216,8 +239,12 @@ public void testSerialization() throws Exception
216239
mapper.registerSubtypes(new NamedType(SubB.class, "typeB"));
217240
assertEquals("{\"@type\":\"typeB\",\"b\":1}", mapper.writeValueAsString(bean));
218241

242+
// the first registered type name is used for serialization
243+
mapper.registerSubtypes(new NamedType(SubB.class, "ignoredOnSerialization"));
244+
assertEquals("{\"@type\":\"typeB\",\"b\":1}", mapper.writeValueAsString(bean));
245+
219246
// and default name ought to be simple class name; with context
220-
assertEquals("{\"@type\":\"TestSubtypes$SubD\",\"d\":0}", mapper.writeValueAsString(new SubD()));
247+
assertEquals("{\"@type\":\"TestSubtypes$SubD\",\"d\":0}", mapper.writeValueAsString(new SubD()));
221248
}
222249

223250
public void testDeserializationNonNamed() throws Exception
@@ -236,6 +263,7 @@ public void testDeserializatioNamed() throws Exception
236263
ObjectMapper mapper = new ObjectMapper();
237264
mapper.registerSubtypes(SubB.class);
238265
mapper.registerSubtypes(new NamedType(SubD.class, "TypeD"));
266+
mapper.registerSubtypes(new NamedType(SubD.class, "typeD"));
239267

240268
SuperType bean = mapper.readValue("{\"@type\":\"TypeB\", \"b\":13}", SuperType.class);
241269
assertSame(SubB.class, bean.getClass());
@@ -245,6 +273,24 @@ public void testDeserializatioNamed() throws Exception
245273
bean = mapper.readValue("{\"@type\":\"TypeD\", \"d\":-4}", SuperType.class);
246274
assertSame(SubD.class, bean.getClass());
247275
assertEquals(-4, ((SubD) bean).d);
276+
277+
// we can register the same subtype under two names
278+
bean = mapper.readValue("{\"@type\":\"typeD\", \"d\":-4}", SuperType.class);
279+
assertSame(SubD.class, bean.getClass());
280+
assertEquals(-4, ((SubD) bean).d);
281+
282+
}
283+
284+
public void testDeserializationWithDuplicateRegisteredSubtypes()
285+
throws Exception {
286+
ObjectMapper mapper = new ObjectMapper();
287+
mapper.registerSubtypes(new NamedType(Sub.class, "sub1"));
288+
mapper.registerSubtypes(new NamedType(Sub.class, "sub2"));
289+
290+
POJOWrapper pojoWrapper = mapper.readValue("{\"sub1\":{\"#type\":\"sub1\",\"a\":10},\"sub2\":{\"#type\":\"sub2\",\"a\":50}}", POJOWrapper.class);
291+
292+
assertEquals(10, pojoWrapper.sub1.a);
293+
assertEquals(50, pojoWrapper.sub2.a);
248294
}
249295

250296
// Trying to reproduce [JACKSON-366]
@@ -295,7 +341,7 @@ public void testDefaultImpl() throws Exception
295341
public void testDefaultImplViaModule() throws Exception
296342
{
297343
final String JSON = "{\"a\":123}";
298-
344+
299345
// first: without registration etc, epic fail:
300346
try {
301347
MAPPER.readValue(JSON, SuperTypeWithoutDefault.class);
@@ -317,7 +363,7 @@ public void testDefaultImplViaModule() throws Exception
317363
bean = mapper.readValue("{\"#type\":\"foobar\"}", SuperTypeWithoutDefault.class);
318364
assertEquals(DefaultImpl505.class, bean.getClass());
319365
assertEquals(0, ((DefaultImpl505) bean).a);
320-
366+
321367
}
322368

323369
public void testErrorMessage() throws Exception {
@@ -361,7 +407,7 @@ public void testSubclassLimits() throws Exception
361407
public void testIssue1125NonDefault() throws Exception
362408
{
363409
String json = MAPPER.writeValueAsString(new Issue1125Wrapper(new Impl1125(1, 2, 3)));
364-
410+
365411
Issue1125Wrapper result = MAPPER.readValue(json, Issue1125Wrapper.class);
366412
assertNotNull(result.value);
367413
assertEquals(Impl1125.class, result.value.getClass());
@@ -374,7 +420,7 @@ public void testIssue1125NonDefault() throws Exception
374420
public void testIssue1125WithDefault() throws Exception
375421
{
376422
Issue1125Wrapper result = MAPPER.readValue(aposToQuotes("{'value':{'a':3,'def':9,'b':5}}"),
377-
Issue1125Wrapper.class);
423+
Issue1125Wrapper.class);
378424
assertNotNull(result.value);
379425
assertEquals(Default1125.class, result.value.getClass());
380426
Default1125 impl = (Default1125) result.value;

0 commit comments

Comments
 (0)