Skip to content

Commit 7835917

Browse files
authored
Fix #4733: handle polymorphic typing better for enums (#4780)
1 parent aea1ccc commit 7835917

File tree

7 files changed

+181
-13
lines changed

7 files changed

+181
-13
lines changed

release-notes/VERSION-2.x

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Project: jackson-databind
44
=== Releases ===
55
------------------------------------------------------------------------
66

7+
2.18.2 (not yet released)
8+
9+
#4733: Wrong serialization of Type Ids for certain types of Enum values
10+
(reported by @nlisker)
11+
712
2.18.1 (28-Oct-2024)
813

914
#4741: When `Include.NON_DEFAULT` setting is used on POJO, empty values

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

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,7 @@ protected JavaType _typeFromId(String id, DatabindContext ctxt) throws IOExcepti
9191

9292
protected String _idFrom(Object value, Class<?> cls, TypeFactory typeFactory)
9393
{
94-
// Need to ensure that "enum subtypes" work too
95-
if (ClassUtil.isEnumType(cls)) {
96-
// 29-Sep-2019, tatu: `Class.isEnum()` only returns true for main declaration,
97-
// but NOT from sub-class thereof (extending individual values). This
98-
// is why additional resolution is needed: we want class that contains
99-
// enumeration instances.
100-
if (!cls.isEnum()) {
101-
// and this parent would then have `Enum.class` as its parent:
102-
cls = cls.getSuperclass();
103-
}
104-
}
94+
cls = _resolveToParentAsNecessary(cls);
10595
String str = cls.getName();
10696
if (str.startsWith(JAVA_UTIL_PKG)) {
10797
// 25-Jan-2009, tatu: There are some internal classes that we cannot access as is.

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,23 @@ public static MinimalClassNameIdResolver construct(JavaType baseType, MapperConf
5757
@Override
5858
public String idFromValue(Object value)
5959
{
60-
String n = value.getClass().getName();
60+
return idFromValueAndType(value, value.getClass());
61+
}
62+
63+
@Override
64+
public String idFromValueAndType(Object value, Class<?> rawType) {
65+
// 04-Nov-2024, tatu: [databind#4733] Need to resolve enum sub-classes
66+
// same way "ClassNameIdResolver" does
67+
rawType = _resolveToParentAsNecessary(rawType);
68+
String n = rawType.getName();
6169
if (n.startsWith(_basePackagePrefix)) {
6270
// note: we will leave the leading dot in there
6371
return n.substring(_basePackagePrefix.length()-1);
6472
}
6573
return n;
74+
6675
}
67-
76+
6877
@Override
6978
protected JavaType _typeFromId(String id, DatabindContext ctxt) throws IOException
7079
{

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ protected String idFromClass(Class<?> clazz)
120120
if (clazz == null) {
121121
return null;
122122
}
123+
// 04-Nov-2024, tatu: [databind#4733] Need to resolve enum sub-classes
124+
// same way "ClassNameIdResolver" does
125+
clazz = _resolveToParentAsNecessary(clazz);
126+
123127
// NOTE: although we may need to let `TypeModifier` change actual type to use
124128
// for id, we can use original type as key for more efficient lookup:
125129
final String key = clazz.getName();

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.fasterxml.jackson.databind.JavaType;
77
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
88
import com.fasterxml.jackson.databind.type.TypeFactory;
9+
import com.fasterxml.jackson.databind.util.ClassUtil;
910

1011
/**
1112
* Partial base implementation of {@link TypeIdResolver}: all custom implementations
@@ -69,4 +70,32 @@ public JavaType typeFromId(DatabindContext context, String id) throws IOExcepti
6970
public String getDescForKnownTypeIds() {
7071
return null;
7172
}
73+
74+
/**
75+
* Helper method for ensuring we properly resolve cases where we don't
76+
* want to use given instance class due to it being a specific inner class
77+
* but rather enclosing (or parent) class. Specific case we know of
78+
* currently are "enum subtypes", cases
79+
* where simple Enum constant has overrides and uses generated sub-class
80+
* if parent Enum type. In this case we need to ensure that we use
81+
* the main/parent Enum type, not sub-class.
82+
*
83+
* @param cls Class to check and possibly resolve
84+
* @return Resolved class to use
85+
* @since 2.18.2
86+
*/
87+
protected Class<?> _resolveToParentAsNecessary(Class<?> cls) {
88+
// Need to ensure that "enum subtypes" work too
89+
if (ClassUtil.isEnumType(cls)) {
90+
// 29-Sep-2019, tatu: `Class.isEnum()` only returns true for main declaration,
91+
// but NOT from sub-class thereof (extending individual values). This
92+
// is why additional resolution is needed: we want class that contains
93+
// enumeration instances.
94+
if (!cls.isEnum()) {
95+
// and this parent would then have `Enum.class` as its parent:
96+
cls = cls.getSuperclass();
97+
}
98+
}
99+
return cls;
100+
}
72101
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ protected String idFromClass(Class<?> clazz)
120120
if (clazz == null) {
121121
return null;
122122
}
123+
// 04-Nov-2024, tatu: [databind#4733] Need to resolve enum sub-classes
124+
// same way "ClassNameIdResolver" does
125+
clazz = _resolveToParentAsNecessary(clazz);
126+
123127
// NOTE: although we may need to let `TypeModifier` change actual type to use
124128
// for id, we can use original type as key for more efficient lookup:
125129
final String key = clazz.getName();
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package com.fasterxml.jackson.databind.jsontype.jdk;
2+
3+
import com.fasterxml.jackson.annotation.*;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
5+
6+
import com.fasterxml.jackson.databind.*;
7+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
11+
import org.junit.jupiter.api.Test;
12+
13+
public class EnumTyping4733Test extends DatabindTestUtil
14+
{
15+
// Baseline case that already worked
16+
@JsonTypeInfo(use = Id.CLASS)
17+
@JsonSubTypes({
18+
@JsonSubTypes.Type(value = A_CLASS.class),
19+
})
20+
interface InterClass {
21+
default void yes() {}
22+
}
23+
24+
enum A_CLASS implements InterClass {
25+
A1,
26+
A2 {
27+
@Override
28+
public void yes() { }
29+
};
30+
}
31+
32+
// Failed before fix for [databind#4733]
33+
@JsonTypeInfo(use = Id.MINIMAL_CLASS)
34+
@JsonSubTypes({
35+
@JsonSubTypes.Type(value = A_MIN_CLASS.class),
36+
})
37+
interface InterMinimalClass {
38+
default void yes() {}
39+
}
40+
41+
enum A_MIN_CLASS implements InterMinimalClass {
42+
A1,
43+
A2 {
44+
@Override
45+
public void yes() { }
46+
};
47+
}
48+
49+
// Failed before fix for [databind#4733]
50+
@JsonTypeInfo(use = Id.NAME)
51+
@JsonSubTypes({
52+
@JsonSubTypes.Type(value = A_NAME.class),
53+
})
54+
interface InterName {
55+
default void yes() {}
56+
}
57+
58+
enum A_NAME implements InterName {
59+
A1,
60+
A2 {
61+
@Override
62+
public void yes() { }
63+
};
64+
}
65+
66+
// Failed before fix for [databind#4733]
67+
@JsonTypeInfo(use = Id.SIMPLE_NAME)
68+
@JsonSubTypes({
69+
@JsonSubTypes.Type(value = A_SIMPLE_NAME.class),
70+
})
71+
interface InterSimpleName {
72+
default void yes() {}
73+
}
74+
75+
enum A_SIMPLE_NAME implements InterSimpleName {
76+
A1,
77+
A2 {
78+
@Override
79+
public void yes() { }
80+
};
81+
}
82+
83+
private final ObjectMapper MAPPER = newJsonMapper();
84+
85+
@Test
86+
public void testIssue4733Class() throws Exception
87+
{
88+
String json1 = MAPPER.writeValueAsString(A_CLASS.A1);
89+
String json2 = MAPPER.writeValueAsString(A_CLASS.A2);
90+
91+
assertEquals(A_CLASS.A1, MAPPER.readValue(json1, A_CLASS.class));
92+
assertEquals(A_CLASS.A2, MAPPER.readValue(json2, A_CLASS.class));
93+
}
94+
95+
@Test
96+
public void testIssue4733MinimalClass() throws Exception
97+
{
98+
String json1 = MAPPER.writeValueAsString(A_MIN_CLASS.A1);
99+
String json2 = MAPPER.writeValueAsString(A_MIN_CLASS.A2);
100+
assertEquals(A_MIN_CLASS.A1, MAPPER.readValue(json1, A_MIN_CLASS.class),
101+
"JSON: "+json1);
102+
assertEquals(A_MIN_CLASS.A2, MAPPER.readValue(json2, A_MIN_CLASS.class),
103+
"JSON: "+json2);
104+
}
105+
106+
@Test
107+
public void testIssue4733Name() throws Exception
108+
{
109+
String json1 = MAPPER.writeValueAsString(A_NAME.A1);
110+
String json2 = MAPPER.writeValueAsString(A_NAME.A2);
111+
assertEquals(A_NAME.A1, MAPPER.readValue(json1, A_NAME.class),
112+
"JSON: "+json1);
113+
assertEquals(A_NAME.A2, MAPPER.readValue(json2, A_NAME.class),
114+
"JSON: "+json2);
115+
}
116+
117+
@Test
118+
public void testIssue4733SimpleName() throws Exception
119+
{
120+
String json1 = MAPPER.writeValueAsString(A_SIMPLE_NAME.A1);
121+
String json2 = MAPPER.writeValueAsString(A_SIMPLE_NAME.A2);
122+
assertEquals(A_SIMPLE_NAME.A1, MAPPER.readValue(json1, A_SIMPLE_NAME.class),
123+
"JSON: "+json1);
124+
assertEquals(A_SIMPLE_NAME.A2, MAPPER.readValue(json2, A_SIMPLE_NAME.class),
125+
"JSON: "+json2);
126+
}
127+
}

0 commit comments

Comments
 (0)