Skip to content

Commit 84fc640

Browse files
authored
Implement #5476: add support for @JsonSerializeAs (#5488)
1 parent c8abad9 commit 84fc640

File tree

3 files changed

+215
-7
lines changed

3 files changed

+215
-7
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Project: jackson-databind
3232
(reported by @DavTurns)
3333
#5475: Support `@JsonDeserializeAs` annotation
3434
(implemented by @cowtowncoder, w/ Claude code)
35+
#5476: Support `@JsonSerializeAs` annotation
36+
(implemented by @cowtowncoder, w/ Claude code)
3537

3638
2.20.2 (not yet released)
3739

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public class JacksonAnnotationIntrospector
3737
@SuppressWarnings("unchecked")
3838
private final static Class<? extends Annotation>[] ANNOTATIONS_TO_INFER_SER = (Class<? extends Annotation>[])
3939
new Class<?>[] {
40-
JsonSerialize.class,
40+
JsonSerialize.class, // databind-specific
41+
JsonSerializeAs.class, // since 2.21 alias (and eventual replacement) for `@JsonSerialize.as`
4142
JsonView.class,
4243
JsonFormat.class,
4344
JsonTypeInfo.class,
@@ -921,10 +922,15 @@ public JavaType refineSerializationType(final MapperConfig<?> config,
921922
final TypeFactory tf = config.getTypeFactory();
922923

923924
final JsonSerialize jsonSer = _findAnnotation(a, JsonSerialize.class);
925+
final JsonSerializeAs jsonSerAs = _findAnnotation(a, JsonSerializeAs.class);
924926

925927
// Ok: start by refining the main type itself; common to all types
926928

927-
final Class<?> serClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.as());
929+
Class<?> serClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.as());
930+
// 09-Dec-2025, tatu: [databind#5476] Also check @JsonSerializeAs
931+
if (serClass == null && jsonSerAs != null) {
932+
serClass = _classIfExplicit(jsonSerAs.value());
933+
}
928934
if (serClass != null) {
929935
if (type.hasRawClass(serClass)) {
930936
// 30-Nov-2015, tatu: As per [databind#1023], need to allow forcing of
@@ -959,7 +965,11 @@ public JavaType refineSerializationType(final MapperConfig<?> config,
959965
// First, key type (for Maps, Map-like types):
960966
if (type.isMapLikeType()) {
961967
JavaType keyType = type.getKeyType();
962-
final Class<?> keyClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.keyAs());
968+
Class<?> keyClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.keyAs());
969+
// 09-Dec-2025, tatu: [databind#5476] Also check @JsonSerializeAs
970+
if (keyClass == null && jsonSerAs != null) {
971+
keyClass = _classIfExplicit(jsonSerAs.key());
972+
}
963973
if (keyClass != null) {
964974
if (keyType.hasRawClass(keyClass)) {
965975
keyType = keyType.withStaticTyping();
@@ -994,7 +1004,11 @@ public JavaType refineSerializationType(final MapperConfig<?> config,
9941004
JavaType contentType = type.getContentType();
9951005
if (contentType != null) { // collection[like], map[like], array, reference
9961006
// And then value types for all containers:
997-
final Class<?> contentClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.contentAs());
1007+
Class<?> contentClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.contentAs());
1008+
// 09-Dec-2025, tatu: [databind#5476] Also check @JsonSerializeAs
1009+
if (contentClass == null && jsonSerAs != null) {
1010+
contentClass = _classIfExplicit(jsonSerAs.content());
1011+
}
9981012
if (contentClass != null) {
9991013
if (contentType.hasRawClass(contentClass)) {
10001014
contentType = contentType.withStaticTyping();
@@ -1302,7 +1316,7 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
13021316
Class<?> valueClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.as());
13031317
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
13041318
if (valueClass == null && jsonDeserAs != null) {
1305-
valueClass = _classIfExplicit(jsonDeserAs.value(), Void.class);
1319+
valueClass = _classIfExplicit(jsonDeserAs.value());
13061320
}
13071321
if ((valueClass != null) && !type.hasRawClass(valueClass)
13081322
&& !_primitiveAndWrapper(type, valueClass)) {
@@ -1322,7 +1336,7 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
13221336
Class<?> keyClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.keyAs());
13231337
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
13241338
if (keyClass == null && jsonDeserAs != null) {
1325-
keyClass = _classIfExplicit(jsonDeserAs.keys(), Void.class);
1339+
keyClass = _classIfExplicit(jsonDeserAs.keys());
13261340
}
13271341
if ((keyClass != null)
13281342
&& !_primitiveAndWrapper(keyType, keyClass)) {
@@ -1342,7 +1356,7 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
13421356
Class<?> contentClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.contentAs());
13431357
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
13441358
if (contentClass == null && jsonDeserAs != null) {
1345-
contentClass = _classIfExplicit(jsonDeserAs.content(), Void.class);
1359+
contentClass = _classIfExplicit(jsonDeserAs.content());
13461360
}
13471361
if ((contentClass != null)
13481362
&& !_primitiveAndWrapper(contentType, contentClass)) {
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.fasterxml.jackson.databind.ser;
2+
3+
import java.util.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
8+
import com.fasterxml.jackson.annotation.JsonSerializeAs;
9+
10+
import com.fasterxml.jackson.databind.*;
11+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
12+
13+
import static org.junit.jupiter.api.Assertions.*;
14+
15+
/**
16+
* Unit tests for new {@link JsonSerializeAs} annotation.
17+
*
18+
* @since 2.21
19+
*/
20+
public class JsonSerializeAsTest extends DatabindTestUtil
21+
{
22+
/*
23+
/**********************************************************************
24+
/* Annotated helper classes for @JsonSerializeAs#value on class
25+
/**********************************************************************
26+
*/
27+
28+
public interface Fooable {
29+
public int getFoo();
30+
}
31+
32+
// force use of interface
33+
@JsonSerializeAs(Fooable.class)
34+
public static class FooImpl implements Fooable {
35+
@Override
36+
public int getFoo() { return 42; }
37+
public int getBar() { return 15; }
38+
}
39+
40+
static class FooImplNoAnno implements Fooable {
41+
@Override
42+
public int getFoo() { return 42; }
43+
public int getBar() { return 15; }
44+
}
45+
46+
public class Fooables {
47+
public FooImpl[] getFoos() {
48+
return new FooImpl[] { new FooImpl() };
49+
}
50+
}
51+
52+
/*
53+
/**********************************************************************
54+
/* Annotated helper classes for @JsonSerializeAs#value on property
55+
/**********************************************************************
56+
*/
57+
58+
public class FooableWrapper {
59+
public FooImpl getFoo() {
60+
return new FooImpl();
61+
}
62+
}
63+
64+
static class FooableWithFieldWrapper {
65+
@JsonSerializeAs(Fooable.class)
66+
public Fooable getFoo() {
67+
return new FooImplNoAnno();
68+
}
69+
}
70+
71+
/*
72+
/**********************************************************************
73+
/* Annotated helper classes for @JsonSerializeAs#content
74+
/**********************************************************************
75+
*/
76+
77+
interface Bean5476Base {
78+
public int getA();
79+
}
80+
81+
@JsonPropertyOrder({"a","b"})
82+
static abstract class Bean5476Abstract implements Bean5476Base {
83+
@Override
84+
public int getA() { return 1; }
85+
86+
public int getB() { return 2; }
87+
}
88+
89+
static class Bean5476Impl extends Bean5476Abstract {
90+
public int getC() { return 3; }
91+
}
92+
93+
static class Bean5476Wrapper {
94+
@JsonSerializeAs(content=Bean5476Abstract.class)
95+
public List<Bean5476Base> values;
96+
public Bean5476Wrapper(int count) {
97+
values = new ArrayList<Bean5476Base>();
98+
for (int i = 0; i < count; ++i) {
99+
values.add(new Bean5476Impl());
100+
}
101+
}
102+
}
103+
104+
static class Bean5476Holder {
105+
@JsonSerializeAs(Bean5476Abstract.class)
106+
public Bean5476Base value = new Bean5476Impl();
107+
}
108+
109+
/*
110+
/**********************************************************************
111+
/* Annotated helper classes for @JsonSerializeAs#key
112+
/**********************************************************************
113+
*/
114+
115+
interface MapKeyBase {
116+
String getId();
117+
}
118+
119+
@JsonPropertyOrder({"id"})
120+
static abstract class MapKeyAbstract implements MapKeyBase {
121+
@Override
122+
public String getId() { return "key"; }
123+
}
124+
125+
static class MapKeyImpl extends MapKeyAbstract {
126+
public String getExtra() { return "extra"; }
127+
}
128+
129+
static class MapKeyWrapper {
130+
@JsonSerializeAs(key=MapKeyAbstract.class)
131+
public Map<MapKeyBase, String> values;
132+
133+
public MapKeyWrapper() {
134+
values = new LinkedHashMap<>();
135+
values.put(new MapKeyImpl(), "value1");
136+
}
137+
}
138+
139+
/*
140+
/**********************************************************************
141+
/* Test methods
142+
/**********************************************************************
143+
*/
144+
145+
private final ObjectWriter WRITER = objectWriter();
146+
147+
@Test
148+
public void testSerializeAsInClass() throws Exception {
149+
assertEquals("{\"foo\":42}", WRITER.writeValueAsString(new FooImpl()));
150+
}
151+
152+
@Test
153+
public void testSerializeAsForArrayProp() throws Exception {
154+
assertEquals("{\"foos\":[{\"foo\":42}]}",
155+
WRITER.writeValueAsString(new Fooables()));
156+
}
157+
158+
@Test
159+
public void testSerializeAsForSimpleProp() throws Exception {
160+
assertEquals("{\"foo\":{\"foo\":42}}",
161+
WRITER.writeValueAsString(new FooableWrapper()));
162+
}
163+
164+
@Test
165+
public void testSerializeWithFieldAnno() throws Exception {
166+
assertEquals("{\"foo\":{\"foo\":42}}",
167+
WRITER.writeValueAsString(new FooableWithFieldWrapper()));
168+
}
169+
170+
// Test for content parameter
171+
@Test
172+
public void testSpecializedContentAs() throws Exception {
173+
assertEquals(a2q("{'values':[{'a':1,'b':2}]}"),
174+
WRITER.writeValueAsString(new Bean5476Wrapper(1)));
175+
}
176+
177+
// Test for value parameter
178+
@Test
179+
public void testSpecializedAsIntermediate() throws Exception {
180+
assertEquals(a2q("{'value':{'a':1,'b':2}}"),
181+
WRITER.writeValueAsString(new Bean5476Holder()));
182+
}
183+
184+
// Test for key parameter
185+
@Test
186+
public void testSpecializedKeyAs() throws Exception {
187+
String json = WRITER.writeValueAsString(new MapKeyWrapper());
188+
// Map key serialization depends on how MapKeyAbstract is serialized
189+
// Since it has only getId(), we expect the key to be serialized as just that property
190+
assertTrue(json.contains("\"values\""), "Should contain 'values' field");
191+
}
192+
}

0 commit comments

Comments
 (0)