Skip to content

Commit 5814d14

Browse files
committed
Merge branch '2.x' into 3.x
2 parents 1e253a6 + c8abad9 commit 5814d14

File tree

5 files changed

+380
-14
lines changed

5 files changed

+380
-14
lines changed

release-notes/VERSION

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Versions: 3.x (for earlier see VERSION-2.x)
4646
#5456: Additional configuration (`JsonNodeFeature.STRIP_TRAILING_BIGDECIMAL_ZEROES`: true)
4747
to MapperBuilder#configureForJackson2 to closer match Jackson 2 behavior
4848
(contributed by @nrayburn-tech)
49+
#5475: Support `@JsonDeserializeAs` annotation
50+
(implemented by @cowtowncoder, w/ Claude code)
4951

5052
3.0.3 (28-Nov-2025)
5153

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/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ public class JacksonAnnotationIntrospector
4949
@SuppressWarnings("unchecked")
5050
private final static Class<? extends Annotation>[] ANNOTATIONS_TO_INFER_DESER = (Class<? extends Annotation>[])
5151
new Class<?>[] {
52-
JsonDeserialize.class,
52+
JsonDeserialize.class, // databind-specific
53+
JsonDeserializeAs.class, // since 2.21 alias (and eventual replacement) for `@JsonDeserialize.as`
5354
JsonView.class,
5455
JsonFormat.class,
5556
JsonTypeInfo.class,
5657
JsonUnwrapped.class,
5758
JsonBackReference.class,
5859
JsonManagedReference.class,
59-
JsonMerge.class // since 2.9
60+
JsonMerge.class
6061
};
6162

6263
// NOTE: To avoid mandatory Module dependency to "java.beans", support for 2
@@ -1245,9 +1246,14 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
12451246
final TypeFactory tf = config.getTypeFactory();
12461247

12471248
final JsonDeserialize jsonDeser = _findAnnotation(a, JsonDeserialize.class);
1249+
final JsonDeserializeAs jsonDeserAs = _findAnnotation(a, JsonDeserializeAs.class);
12481250

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

0 commit comments

Comments
 (0)