Skip to content

Commit c8abad9

Browse files
authored
Fix #5475: add support for @JsonDeserializeAs (#5484)
1 parent 9689f56 commit c8abad9

File tree

4 files changed

+376
-14
lines changed

4 files changed

+376
-14
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Project: jackson-databind
3030
(contributed by Hélios G)
3131
#5429: Formatting and Parsing of Large ISO-8601 Dates is inconsistent
3232
(reported by @DavTurns)
33+
#5475: Support `@JsonDeserializeAs` annotation
34+
(implemented by @cowtowncoder, w/ Claude code)
3335

3436
2.20.2 (not yet released)
3537

src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public class JacksonAnnotationIntrospector
5050
@SuppressWarnings("unchecked")
5151
private final static Class<? extends Annotation>[] ANNOTATIONS_TO_INFER_DESER = (Class<? extends Annotation>[])
5252
new Class<?>[] {
53-
JsonDeserialize.class,
53+
JsonDeserialize.class, // databind-specific
54+
JsonDeserializeAs.class, // since 2.21 alias (and eventual replacement) for `@JsonDeserialize.as`
5455
JsonView.class,
5556
JsonFormat.class,
5657
JsonTypeInfo.class,
@@ -1295,9 +1296,14 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
12951296
final TypeFactory tf = config.getTypeFactory();
12961297

12971298
final JsonDeserialize jsonDeser = _findAnnotation(a, JsonDeserialize.class);
1299+
final JsonDeserializeAs jsonDeserAs = _findAnnotation(a, JsonDeserializeAs.class);
12981300

12991301
// Ok: start by refining the main type itself; common to all types
1300-
final Class<?> valueClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.as());
1302+
Class<?> valueClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.as());
1303+
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
1304+
if (valueClass == null && jsonDeserAs != null) {
1305+
valueClass = _classIfExplicit(jsonDeserAs.value(), Void.class);
1306+
}
13011307
if ((valueClass != null) && !type.hasRawClass(valueClass)
13021308
&& !_primitiveAndWrapper(type, valueClass)) {
13031309
try {
@@ -1313,7 +1319,11 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
13131319
// First, key type (for Maps, Map-like types):
13141320
if (type.isMapLikeType()) {
13151321
JavaType keyType = type.getKeyType();
1316-
final Class<?> keyClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.keyAs());
1322+
Class<?> keyClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.keyAs());
1323+
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
1324+
if (keyClass == null && jsonDeserAs != null) {
1325+
keyClass = _classIfExplicit(jsonDeserAs.keys(), Void.class);
1326+
}
13171327
if ((keyClass != null)
13181328
&& !_primitiveAndWrapper(keyType, keyClass)) {
13191329
try {
@@ -1329,7 +1339,11 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
13291339
JavaType contentType = type.getContentType();
13301340
if (contentType != null) { // collection[like], map[like], array, reference
13311341
// And then value types for all containers:
1332-
final Class<?> contentClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.contentAs());
1342+
Class<?> contentClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.contentAs());
1343+
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
1344+
if (contentClass == null && jsonDeserAs != null) {
1345+
contentClass = _classIfExplicit(jsonDeserAs.content(), Void.class);
1346+
}
13331347
if ((contentClass != null)
13341348
&& !_primitiveAndWrapper(contentType, contentClass)) {
13351349
try {
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package com.fasterxml.jackson.databind.deser;
2+
3+
import java.util.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import com.fasterxml.jackson.annotation.JsonDeserializeAs;
8+
import com.fasterxml.jackson.databind.*;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
import static com.fasterxml.jackson.databind.testutil.DatabindTestUtil.newJsonMapper;
13+
14+
/**
15+
* Unit tests for new {@link JsonDeserializeAs} annotation.
16+
*
17+
* @since 2.21
18+
*/
19+
public class JsonDeserializeAsTest
20+
{
21+
/*
22+
/**********************************************************************
23+
/* Annotated root classes for @JsonDeserializeAs
24+
/**********************************************************************
25+
*/
26+
27+
@JsonDeserializeAs(RootInterfaceImpl.class)
28+
interface RootInterface {
29+
public String getA();
30+
}
31+
32+
static class RootInterfaceImpl implements RootInterface {
33+
public String a;
34+
35+
public RootInterfaceImpl() { }
36+
37+
@Override
38+
public String getA() { return a; }
39+
}
40+
41+
/*
42+
/**********************************************************************
43+
/* Annotated helper classes for @JsonDeserializeAs#value
44+
/**********************************************************************
45+
*/
46+
47+
// Class for testing valid {@link JsonDeserializeAs} annotation
48+
// with 'value' parameter to define concrete class to deserialize to
49+
static class CollectionHolder
50+
{
51+
Collection<String> _strings;
52+
53+
/* Default for 'Collection' would probably be ArrayList or so;
54+
* let's try to make it a TreeSet instead.
55+
*/
56+
@JsonDeserializeAs(TreeSet.class)
57+
public void setStrings(Collection<String> s)
58+
{
59+
_strings = s;
60+
}
61+
}
62+
63+
// Another class for testing valid {@link JsonDeserializeAs} annotation
64+
// with 'value' parameter to define concrete class to deserialize to
65+
static class MapHolder
66+
{
67+
// Let's also coerce numbers into Strings here
68+
Map<String,String> _data;
69+
70+
/* Default for 'Map' would be HashMap,
71+
* let's try to make it a TreeMap instead.
72+
*/
73+
@JsonDeserializeAs(TreeMap.class)
74+
public void setStrings(Map<String,String> s)
75+
{
76+
_data = s;
77+
}
78+
}
79+
80+
// Another class for testing valid {@link JsonDeserializeAs} annotation
81+
// with 'value' parameter, but with array
82+
static class ArrayHolder
83+
{
84+
String[] _strings;
85+
86+
@JsonDeserializeAs(String[].class)
87+
public void setStrings(Object[] o)
88+
{
89+
// should be passed instances of proper type, as per annotation
90+
_strings = (String[]) o;
91+
}
92+
}
93+
94+
/*
95+
/**********************************************************************
96+
/* Annotated helper classes for @JsonDeserializeAs#keys
97+
/**********************************************************************
98+
*/
99+
100+
static class StringWrapper
101+
{
102+
final String _string;
103+
104+
public StringWrapper(String s) { _string = s; }
105+
}
106+
107+
static class MapKeyHolder
108+
{
109+
Map<Object, String> _map;
110+
111+
@JsonDeserializeAs(keys=StringWrapper.class)
112+
public void setMap(Map<Object,String> m)
113+
{
114+
// type should be ok, but no need to cast here (won't matter)
115+
_map = m;
116+
}
117+
}
118+
119+
/*
120+
/**********************************************************************
121+
/* Annotated helper classes for @JsonDeserializeAs#content
122+
/**********************************************************************
123+
*/
124+
125+
static class ListContentHolder
126+
{
127+
List<?> _list;
128+
129+
@JsonDeserializeAs(content=StringWrapper.class)
130+
public void setList(List<?> l) {
131+
_list = l;
132+
}
133+
}
134+
135+
static class ArrayContentHolder
136+
{
137+
Object[] _data;
138+
139+
@JsonDeserializeAs(content=Long.class)
140+
public void setData(Object[] o)
141+
{ // should have proper type, but no need to coerce here
142+
_data = o;
143+
}
144+
}
145+
146+
static class MapContentHolder
147+
{
148+
Map<Object,Object> _map;
149+
150+
@JsonDeserializeAs(content=Integer.class)
151+
public void setMap(Map<Object,Object> m)
152+
{
153+
_map = m;
154+
}
155+
}
156+
157+
/*
158+
/**********************************************************
159+
/* Test methods for @JsonDeserializeAs#value
160+
/**********************************************************
161+
*/
162+
163+
private final ObjectMapper MAPPER = newJsonMapper();
164+
165+
@Test
166+
public void testOverrideClassValid() throws Exception
167+
{
168+
CollectionHolder result = MAPPER.readValue
169+
("{ \"strings\" : [ \"test\" ] }", CollectionHolder.class);
170+
171+
Collection<String> strs = result._strings;
172+
assertEquals(1, strs.size());
173+
assertEquals(TreeSet.class, strs.getClass());
174+
assertEquals("test", strs.iterator().next());
175+
}
176+
177+
@Test
178+
public void testOverrideMapValid() throws Exception
179+
{
180+
// note: expecting conversion from number to String, as well
181+
MapHolder result = MAPPER.readValue
182+
("{ \"strings\" : { \"a\" : 3 } }", MapHolder.class);
183+
184+
Map<String,String> strs = result._data;
185+
assertEquals(1, strs.size());
186+
assertEquals(TreeMap.class, strs.getClass());
187+
String value = strs.get("a");
188+
assertEquals("3", value);
189+
}
190+
191+
@Test
192+
public void testOverrideArrayClass() throws Exception
193+
{
194+
ArrayHolder result = MAPPER.readValue
195+
("{ \"strings\" : [ \"test\" ] }", ArrayHolder.class);
196+
197+
String[] strs = result._strings;
198+
assertEquals(1, strs.length);
199+
assertEquals(String[].class, strs.getClass());
200+
assertEquals("test", strs[0]);
201+
}
202+
203+
/*
204+
/**********************************************************
205+
/* Test methods for @JsonDeserializeAs#value used for root values
206+
/**********************************************************
207+
*/
208+
209+
@Test
210+
public void testRootInterfaceAs() throws Exception
211+
{
212+
RootInterface value = MAPPER.readValue("{\"a\":\"abc\" }", RootInterface.class);
213+
assertTrue(value instanceof RootInterfaceImpl);
214+
assertEquals("abc", value.getA());
215+
}
216+
217+
/*
218+
/**********************************************************
219+
/* Test methods for @JsonDeserializeAs#keys
220+
/**********************************************************
221+
*/
222+
223+
@SuppressWarnings("unchecked")
224+
@Test
225+
public void testOverrideKeyClassValid() throws Exception
226+
{
227+
MapKeyHolder result = MAPPER.readValue("{ \"map\" : { \"xxx\" : \"yyy\" } }", MapKeyHolder.class);
228+
Map<StringWrapper, String> map = (Map<StringWrapper,String>)(Map<?,?>)result._map;
229+
assertEquals(1, map.size());
230+
Map.Entry<StringWrapper, String> en = map.entrySet().iterator().next();
231+
232+
StringWrapper key = en.getKey();
233+
assertEquals(StringWrapper.class, key.getClass());
234+
assertEquals("xxx", key._string);
235+
assertEquals("yyy", en.getValue());
236+
}
237+
238+
/*
239+
/**********************************************************
240+
/* Test methods for @JsonDeserializeAs#content
241+
/**********************************************************
242+
*/
243+
244+
@SuppressWarnings("unchecked")
245+
@Test
246+
public void testOverrideContentClassValid() throws Exception
247+
{
248+
ListContentHolder result = MAPPER.readValue("{ \"list\" : [ \"abc\" ] }", ListContentHolder.class);
249+
List<StringWrapper> list = (List<StringWrapper>)result._list;
250+
assertEquals(1, list.size());
251+
Object value = list.get(0);
252+
assertEquals(StringWrapper.class, value.getClass());
253+
assertEquals("abc", ((StringWrapper) value)._string);
254+
}
255+
256+
@Test
257+
public void testOverrideArrayContents() throws Exception
258+
{
259+
ArrayContentHolder result = MAPPER.readValue("{ \"data\" : [ 1, 2, 3 ] }",
260+
ArrayContentHolder.class);
261+
Object[] data = result._data;
262+
assertEquals(3, data.length);
263+
assertEquals(Long[].class, data.getClass());
264+
assertEquals(1L, data[0]);
265+
assertEquals(2L, data[1]);
266+
assertEquals(3L, data[2]);
267+
}
268+
269+
@Test
270+
public void testOverrideMapContents() throws Exception
271+
{
272+
MapContentHolder result = MAPPER.readValue("{ \"map\" : { \"a\" : 9 } }",
273+
MapContentHolder.class);
274+
Map<Object,Object> map = result._map;
275+
assertEquals(1, map.size());
276+
Object ob = map.values().iterator().next();
277+
assertEquals(Integer.class, ob.getClass());
278+
assertEquals(Integer.valueOf(9), ob);
279+
}
280+
}

0 commit comments

Comments
 (0)