Skip to content

Commit 7c18a74

Browse files
authored
Fix #4407: support use of null typeId by custom TypeIdResolver (#4583)
1 parent 451b000 commit 7c18a74

File tree

4 files changed

+303
-6
lines changed

4 files changed

+303
-6
lines changed

release-notes/VERSION-2.x

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Project: jackson-databind
2424
#4356: `BeanDeserializerModifier::updateBuilder()` doesn't work for
2525
beans with Creator methods
2626
(reported by Mark H)
27+
#4407: `null` type id handling does not work with `writeTypePrefix()`
2728
#4452: `@JsonProperty` not serializing field names properly
2829
on `@JsonCreator` in Record
2930
(reported by @Incara)

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
import java.io.IOException;
44

55
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
6+
67
import com.fasterxml.jackson.core.*;
78
import com.fasterxml.jackson.core.util.JsonParserSequence;
9+
810
import com.fasterxml.jackson.databind.*;
911
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
1012
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
13+
import com.fasterxml.jackson.databind.util.ClassUtil;
1114
import com.fasterxml.jackson.databind.util.TokenBuffer;
1215

1316
/**
@@ -136,7 +139,13 @@ protected String _locateTypeId(JsonParser p, DeserializationContext ctxt) throws
136139
// Need to allow even more customized handling, if something unexpected seen...
137140
// but should there be a way to limit this to likely success cases?
138141
if (_defaultImpl != null) {
139-
return _idResolver.idFromBaseType();
142+
String id = _idResolver.idFromBaseType();
143+
if (id == null) {
144+
ctxt.reportBadDefinition(_idResolver.getClass(),
145+
"`idFromBaseType()` (of "
146+
+ClassUtil.classNameOf(_idResolver)+") returned `null`");
147+
}
148+
return id;
140149
}
141150
ctxt.reportWrongTokenException(baseType(), JsonToken.START_ARRAY,
142151
"need Array value to contain `As.WRAPPER_ARRAY` type information for class "+baseTypeName());

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.fasterxml.jackson.annotation.JsonTypeInfo;
66

77
import com.fasterxml.jackson.core.JsonGenerator;
8-
8+
import com.fasterxml.jackson.core.JsonToken;
99
import com.fasterxml.jackson.core.type.WritableTypeId;
1010
import com.fasterxml.jackson.databind.BeanProperty;
1111
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
@@ -44,10 +44,10 @@ public WritableTypeId writeTypePrefix(JsonGenerator g,
4444
{
4545
_generateTypeId(idMetadata);
4646
// 16-Jan-2022, tatu: As per [databind#3373], skip for null typeId.
47-
// And return "null" so that matching "writeTypeSuffix" call should
48-
// be avoided as well.
47+
// And return "null" to avoid matching "writeTypeSuffix" as well.
48+
// 15-Jun-2024, tatu: [databind#4407] Not so fast! Output wrappers
4949
if (idMetadata.id == null) {
50-
return null;
50+
return _writeTypePrefixForNull(g, idMetadata);
5151
}
5252
return g.writeTypePrefix(idMetadata);
5353
}
@@ -57,12 +57,43 @@ public WritableTypeId writeTypeSuffix(JsonGenerator g,
5757
WritableTypeId idMetadata) throws IOException
5858
{
5959
// 16-Jan-2022, tatu: As per [databind#3373], skip for null:
60+
// 15-Jun-2024, tatu: [databind#4407] except no, write closing wrapper
6061
if (idMetadata == null) {
61-
return null;
62+
return _writeTypeSuffixfixForNull(g, idMetadata);
6263
}
6364
return g.writeTypeSuffix(idMetadata);
6465
}
6566

67+
private WritableTypeId _writeTypePrefixForNull(JsonGenerator g,
68+
WritableTypeId typeIdDef) throws IOException
69+
{
70+
// copied from `jackson-core`, `JsonGenerator.writeTypePrefix()`
71+
final JsonToken valueShape = typeIdDef.valueShape;
72+
typeIdDef.wrapperWritten = false;
73+
if (valueShape == JsonToken.START_OBJECT) {
74+
g.writeStartObject(typeIdDef.forValue);
75+
} else if (valueShape == JsonToken.START_ARRAY) {
76+
// should we now set the current object?
77+
g.writeStartArray(typeIdDef.forValue);
78+
}
79+
80+
return typeIdDef;
81+
}
82+
83+
private WritableTypeId _writeTypeSuffixfixForNull(JsonGenerator g,
84+
WritableTypeId typeIdDef) throws IOException
85+
{
86+
// copied from `jackson-core`, `JsonGenerator.writeTypeSuffix()`
87+
final JsonToken valueShape = typeIdDef.valueShape;
88+
// First: does value need closing?
89+
if (valueShape == JsonToken.START_OBJECT) {
90+
g.writeEndObject();
91+
} else if (valueShape == JsonToken.START_ARRAY) {
92+
g.writeEndArray();
93+
}
94+
return typeIdDef;
95+
}
96+
6697
/**
6798
* Helper method that will generate type id to use, if not already passed.
6899
*
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package com.fasterxml.jackson.databind.jsontype;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.annotation.*;
6+
7+
import com.fasterxml.jackson.databind.*;
8+
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
9+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertNotNull;
13+
14+
// for [databind#4407]
15+
public class CustomTypeIdResolver4407Test extends DatabindTestUtil
16+
{
17+
static class Wrapper4407Prop {
18+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
19+
include = JsonTypeInfo.As.PROPERTY,
20+
defaultImpl = Default4407.class,
21+
property = "type")
22+
@JsonTypeIdResolver(Resolver4407_typex.class)
23+
public Base4407 wrapped;
24+
25+
Wrapper4407Prop() { }
26+
public Wrapper4407Prop(String v) {
27+
wrapped = new Impl4407(v);
28+
}
29+
}
30+
31+
static class Wrapper4407PropNull {
32+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
33+
include = JsonTypeInfo.As.PROPERTY,
34+
defaultImpl = Default4407.class,
35+
property = "type")
36+
@JsonTypeIdResolver(Resolver4407_null.class)
37+
public Base4407 wrapped;
38+
39+
Wrapper4407PropNull() { }
40+
public Wrapper4407PropNull(String v) {
41+
wrapped = new Impl4407(v);
42+
}
43+
}
44+
45+
static class Wrapper4407WrapperArray {
46+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
47+
include = JsonTypeInfo.As.WRAPPER_ARRAY,
48+
defaultImpl = Default4407.class)
49+
@JsonTypeIdResolver(Resolver4407_typex.class)
50+
public Base4407 wrapped;
51+
52+
Wrapper4407WrapperArray() { }
53+
public Wrapper4407WrapperArray(String v) {
54+
wrapped = new Impl4407(v);
55+
}
56+
}
57+
58+
static class Wrapper4407WrapperArrayNull {
59+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
60+
include = JsonTypeInfo.As.WRAPPER_ARRAY,
61+
defaultImpl = Default4407.class)
62+
@JsonTypeIdResolver(Resolver4407_null.class)
63+
public Base4407 wrapped;
64+
65+
Wrapper4407WrapperArrayNull() { }
66+
public Wrapper4407WrapperArrayNull(String v) {
67+
wrapped = new Impl4407(v);
68+
}
69+
}
70+
71+
static class Wrapper4407WrapperObject {
72+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
73+
include = JsonTypeInfo.As.WRAPPER_OBJECT,
74+
defaultImpl = Default4407.class)
75+
@JsonTypeIdResolver(Resolver4407_typex.class)
76+
public Base4407 wrapped;
77+
78+
Wrapper4407WrapperObject() { }
79+
public Wrapper4407WrapperObject(String v) {
80+
wrapped = new Impl4407(v);
81+
}
82+
}
83+
84+
static class Wrapper4407WrapperObjectNull {
85+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
86+
include = JsonTypeInfo.As.WRAPPER_OBJECT,
87+
defaultImpl = Default4407.class)
88+
@JsonTypeIdResolver(Resolver4407_null.class)
89+
public Base4407 wrapped;
90+
91+
Wrapper4407WrapperObjectNull() { }
92+
public Wrapper4407WrapperObjectNull(String v) {
93+
wrapped = new Impl4407(v);
94+
}
95+
}
96+
97+
@JsonSubTypes({ @JsonSubTypes.Type(value = Impl4407.class) })
98+
static class Base4407 { }
99+
100+
static class Impl4407 extends Base4407 {
101+
public String value;
102+
103+
Impl4407() { }
104+
public Impl4407(String v) { value = v; }
105+
}
106+
107+
static class Default4407 extends Base4407 {
108+
public String value;
109+
110+
Default4407() { }
111+
public Default4407(String v) { value = v; }
112+
}
113+
114+
static class Resolver4407_typex extends Resolver4407Base {
115+
public Resolver4407_typex() { super("typeX"); }
116+
}
117+
118+
static class Resolver4407_null extends Resolver4407Base {
119+
public Resolver4407_null() { super(null); }
120+
}
121+
122+
static abstract class Resolver4407Base implements TypeIdResolver {
123+
private final String _typeId;
124+
125+
Resolver4407Base(String typeId) {
126+
_typeId = typeId;
127+
}
128+
129+
@Override
130+
public void init(JavaType baseType) { }
131+
132+
@Override
133+
public String idFromValue(Object value) {
134+
return _typeId;
135+
}
136+
137+
@Override
138+
public String idFromValueAndType(Object value, Class<?> suggestedType) {
139+
return idFromValue(value);
140+
}
141+
142+
@Override
143+
public String idFromBaseType() {
144+
// NOTE: needed for trying to deserialize without type id
145+
return "default";
146+
}
147+
148+
@Override
149+
public JavaType typeFromId(DatabindContext ctxt, String id) {
150+
if (id.equals(_typeId)) {
151+
return ctxt.constructType(Impl4407.class);
152+
}
153+
return null;
154+
}
155+
156+
@Override
157+
public String getDescForKnownTypeIds() {
158+
return null;
159+
}
160+
161+
@Override
162+
public JsonTypeInfo.Id getMechanism() {
163+
return JsonTypeInfo.Id.CUSTOM;
164+
}
165+
}
166+
167+
/*
168+
/**********************************************************
169+
/* Unit tests
170+
/**********************************************************
171+
*/
172+
173+
private final ObjectMapper MAPPER = newJsonMapper();
174+
175+
// [databind#4407]: with "as-property" type id
176+
@Test
177+
public void testTypeIdProp4407NonNull() throws Exception
178+
{
179+
// First, check out "normal" case of non-null type id
180+
final String EXP = a2q("{'wrapped':{'type':'typeX','value':'xyz'}}");
181+
assertEquals(EXP,
182+
MAPPER.writeValueAsString(new Wrapper4407Prop("xyz")));
183+
Wrapper4407Prop result = MAPPER.readValue(EXP, Wrapper4407Prop.class);
184+
assertNotNull(result);
185+
assertNotNull(result.wrapped);
186+
assertEquals(Impl4407.class, result.wrapped.getClass());
187+
}
188+
189+
@Test
190+
public void testTypeIdProp4407Null() throws Exception
191+
{
192+
// And then null one
193+
final String EXP = a2q("{'wrapped':{'value':'xyz'}}");
194+
assertEquals(EXP,
195+
MAPPER.writeValueAsString(new Wrapper4407PropNull("xyz")));
196+
assertNotNull(MAPPER.readValue(EXP, Wrapper4407PropNull.class));
197+
Wrapper4407Prop result = MAPPER.readValue(EXP, Wrapper4407Prop.class);
198+
assertNotNull(result);
199+
assertNotNull(result.wrapped);
200+
assertEquals(Default4407.class, result.wrapped.getClass());
201+
}
202+
203+
// [databind#4407]: with "as-wrapper-array" type id
204+
@Test
205+
public void testTypeIdWrapperArray4407NonNull() throws Exception
206+
{
207+
// First, check out "normal" case of non-null type id
208+
final String EXP = a2q("{'wrapped':['typeX',{'value':'xyz'}]}");
209+
assertEquals(EXP,
210+
MAPPER.writeValueAsString(new Wrapper4407WrapperArray("xyz")));
211+
Wrapper4407WrapperArray result = MAPPER.readValue(EXP, Wrapper4407WrapperArray.class);
212+
assertNotNull(result);
213+
assertNotNull(result.wrapped);
214+
assertEquals(Impl4407.class, result.wrapped.getClass());
215+
}
216+
217+
@Test
218+
public void testTypeIdWrapperArray4407Null() throws Exception
219+
{
220+
// And then null one
221+
final String EXP = a2q("{'wrapped':{'value':'xyz'}}");
222+
assertEquals(EXP,
223+
MAPPER.writeValueAsString(new Wrapper4407WrapperArrayNull("xyz")));
224+
Wrapper4407WrapperArray result = MAPPER.readValue(EXP, Wrapper4407WrapperArray.class);
225+
assertNotNull(result);
226+
assertNotNull(result.wrapped);
227+
assertEquals(Default4407.class, result.wrapped.getClass());
228+
}
229+
230+
// [databind#4407]: with "as-wrapper-object" type id
231+
@Test
232+
public void testTypeIdWrapperObject4407NonNull() throws Exception
233+
{
234+
// First, check out "normal" case of non-null type id
235+
final String EXP = a2q("{'wrapped':{'typeX':{'value':'xyz'}}}");
236+
assertEquals(EXP,
237+
MAPPER.writeValueAsString(new Wrapper4407WrapperObject("xyz")));
238+
Wrapper4407WrapperObject result = MAPPER.readValue(EXP, Wrapper4407WrapperObject.class);
239+
assertNotNull(result);
240+
assertNotNull(result.wrapped);
241+
assertEquals(Impl4407.class, result.wrapped.getClass());
242+
}
243+
244+
@Test
245+
public void testTypeIdWrapperObject4407Null() throws Exception
246+
{
247+
// And then null one
248+
final String EXP = a2q("{'wrapped':{'value':'xyz'}}");
249+
assertEquals(EXP,
250+
MAPPER.writeValueAsString(new Wrapper4407WrapperObjectNull("xyz")));
251+
Wrapper4407WrapperObject result = MAPPER.readValue(EXP, Wrapper4407WrapperObject.class);
252+
assertNotNull(result);
253+
assertNotNull(result.wrapped);
254+
assertEquals(Default4407.class, result.wrapped.getClass());
255+
}
256+
}

0 commit comments

Comments
 (0)