Skip to content

Commit 0d38484

Browse files
committed
Merge branch '2.x' into 3.x
2 parents 5814d14 + 84fc640 commit 0d38484

File tree

3 files changed

+213
-7
lines changed

3 files changed

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

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

888889
final JsonSerialize jsonSer = _findAnnotation(a, JsonSerialize.class);
890+
final JsonSerializeAs jsonSerAs = _findAnnotation(a, JsonSerializeAs.class);
889891

890892
// Ok: start by refining the main type itself; common to all types
891893

892-
final Class<?> serClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.as());
894+
Class<?> serClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.as());
895+
// 09-Dec-2025, tatu: [databind#5476] Also check @JsonSerializeAs
896+
if (serClass == null && jsonSerAs != null) {
897+
serClass = _classIfExplicit(jsonSerAs.value());
898+
}
893899
if (serClass != null) {
894900
if (type.hasRawClass(serClass)) {
895901
// 30-Nov-2015, tatu: As per [databind#1023], need to allow forcing of
@@ -924,7 +930,11 @@ public JavaType refineSerializationType(final MapperConfig<?> config,
924930
// First, key type (for Maps, Map-like types):
925931
if (type.isMapLikeType()) {
926932
JavaType keyType = type.getKeyType();
927-
final Class<?> keyClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.keyAs());
933+
Class<?> keyClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.keyAs());
934+
// 09-Dec-2025, tatu: [databind#5476] Also check @JsonSerializeAs
935+
if (keyClass == null && jsonSerAs != null) {
936+
keyClass = _classIfExplicit(jsonSerAs.key());
937+
}
928938
if (keyClass != null) {
929939
if (keyType.hasRawClass(keyClass)) {
930940
keyType = keyType.withStaticTyping();
@@ -959,7 +969,11 @@ public JavaType refineSerializationType(final MapperConfig<?> config,
959969
JavaType contentType = type.getContentType();
960970
if (contentType != null) { // collection[like], map[like], array, reference
961971
// And then value types for all containers:
962-
final Class<?> contentClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.contentAs());
972+
Class<?> contentClass = (jsonSer == null) ? null : _classIfExplicit(jsonSer.contentAs());
973+
// 09-Dec-2025, tatu: [databind#5476] Also check @JsonSerializeAs
974+
if (contentClass == null && jsonSerAs != null) {
975+
contentClass = _classIfExplicit(jsonSerAs.content());
976+
}
963977
if (contentClass != null) {
964978
if (contentType.hasRawClass(contentClass)) {
965979
contentType = contentType.withStaticTyping();
@@ -1252,7 +1266,7 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
12521266
Class<?> valueClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.as());
12531267
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
12541268
if (valueClass == null && jsonDeserAs != null) {
1255-
valueClass = _classIfExplicit(jsonDeserAs.value(), Void.class);
1269+
valueClass = _classIfExplicit(jsonDeserAs.value());
12561270
}
12571271
if ((valueClass != null) && !type.hasRawClass(valueClass)
12581272
&& !_primitiveAndWrapper(type, valueClass)) {
@@ -1272,7 +1286,7 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
12721286
Class<?> keyClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.keyAs());
12731287
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
12741288
if (keyClass == null && jsonDeserAs != null) {
1275-
keyClass = _classIfExplicit(jsonDeserAs.keys(), Void.class);
1289+
keyClass = _classIfExplicit(jsonDeserAs.keys());
12761290
}
12771291
if ((keyClass != null)
12781292
&& !_primitiveAndWrapper(keyType, keyClass)) {
@@ -1292,7 +1306,7 @@ public JavaType refineDeserializationType(final MapperConfig<?> config,
12921306
Class<?> contentClass = (jsonDeser == null) ? null : _classIfExplicit(jsonDeser.contentAs());
12931307
// 09-Dec-2025, tatu: [databind#5475] Also check @JsonDeserializeAs
12941308
if (contentClass == null && jsonDeserAs != null) {
1295-
contentClass = _classIfExplicit(jsonDeserAs.content(), Void.class);
1309+
contentClass = _classIfExplicit(jsonDeserAs.content());
12961310
}
12971311
if ((contentClass != null)
12981312
&& !_primitiveAndWrapper(contentType, contentClass)) {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package tools.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 tools.jackson.databind.*;
11+
import tools.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+
public class JsonSerializeAsTest extends DatabindTestUtil
19+
{
20+
/*
21+
/**********************************************************************
22+
/* Annotated helper classes for @JsonSerializeAs#value on class
23+
/**********************************************************************
24+
*/
25+
26+
public interface Fooable {
27+
public int getFoo();
28+
}
29+
30+
// force use of interface
31+
@JsonSerializeAs(Fooable.class)
32+
public static class FooImpl implements Fooable {
33+
@Override
34+
public int getFoo() { return 42; }
35+
public int getBar() { return 15; }
36+
}
37+
38+
static class FooImplNoAnno implements Fooable {
39+
@Override
40+
public int getFoo() { return 42; }
41+
public int getBar() { return 15; }
42+
}
43+
44+
public class Fooables {
45+
public FooImpl[] getFoos() {
46+
return new FooImpl[] { new FooImpl() };
47+
}
48+
}
49+
50+
/*
51+
/**********************************************************************
52+
/* Annotated helper classes for @JsonSerializeAs#value on property
53+
/**********************************************************************
54+
*/
55+
56+
public class FooableWrapper {
57+
public FooImpl getFoo() {
58+
return new FooImpl();
59+
}
60+
}
61+
62+
static class FooableWithFieldWrapper {
63+
@JsonSerializeAs(Fooable.class)
64+
public Fooable getFoo() {
65+
return new FooImplNoAnno();
66+
}
67+
}
68+
69+
/*
70+
/**********************************************************************
71+
/* Annotated helper classes for @JsonSerializeAs#content
72+
/**********************************************************************
73+
*/
74+
75+
interface Bean5476Base {
76+
public int getA();
77+
}
78+
79+
@JsonPropertyOrder({"a","b"})
80+
static abstract class Bean5476Abstract implements Bean5476Base {
81+
@Override
82+
public int getA() { return 1; }
83+
84+
public int getB() { return 2; }
85+
}
86+
87+
static class Bean5476Impl extends Bean5476Abstract {
88+
public int getC() { return 3; }
89+
}
90+
91+
static class Bean5476Wrapper {
92+
@JsonSerializeAs(content=Bean5476Abstract.class)
93+
public List<Bean5476Base> values;
94+
public Bean5476Wrapper(int count) {
95+
values = new ArrayList<Bean5476Base>();
96+
for (int i = 0; i < count; ++i) {
97+
values.add(new Bean5476Impl());
98+
}
99+
}
100+
}
101+
102+
static class Bean5476Holder {
103+
@JsonSerializeAs(Bean5476Abstract.class)
104+
public Bean5476Base value = new Bean5476Impl();
105+
}
106+
107+
/*
108+
/**********************************************************************
109+
/* Annotated helper classes for @JsonSerializeAs#key
110+
/**********************************************************************
111+
*/
112+
113+
interface MapKeyBase {
114+
String getId();
115+
}
116+
117+
@JsonPropertyOrder({"id"})
118+
static abstract class MapKeyAbstract implements MapKeyBase {
119+
@Override
120+
public String getId() { return "key"; }
121+
}
122+
123+
static class MapKeyImpl extends MapKeyAbstract {
124+
public String getExtra() { return "extra"; }
125+
}
126+
127+
static class MapKeyWrapper {
128+
@JsonSerializeAs(key=MapKeyAbstract.class)
129+
public Map<MapKeyBase, String> values;
130+
131+
public MapKeyWrapper() {
132+
values = new LinkedHashMap<>();
133+
values.put(new MapKeyImpl(), "value1");
134+
}
135+
}
136+
137+
/*
138+
/**********************************************************************
139+
/* Test methods
140+
/**********************************************************************
141+
*/
142+
143+
private final ObjectWriter WRITER = objectWriter();
144+
145+
@Test
146+
public void testSerializeAsInClass() throws Exception {
147+
assertEquals("{\"foo\":42}", WRITER.writeValueAsString(new FooImpl()));
148+
}
149+
150+
@Test
151+
public void testSerializeAsForArrayProp() throws Exception {
152+
assertEquals("{\"foos\":[{\"foo\":42}]}",
153+
WRITER.writeValueAsString(new Fooables()));
154+
}
155+
156+
@Test
157+
public void testSerializeAsForSimpleProp() throws Exception {
158+
assertEquals("{\"foo\":{\"foo\":42}}",
159+
WRITER.writeValueAsString(new FooableWrapper()));
160+
}
161+
162+
@Test
163+
public void testSerializeWithFieldAnno() throws Exception {
164+
assertEquals("{\"foo\":{\"foo\":42}}",
165+
WRITER.writeValueAsString(new FooableWithFieldWrapper()));
166+
}
167+
168+
// Test for content parameter
169+
@Test
170+
public void testSpecializedContentAs() throws Exception {
171+
assertEquals(a2q("{'values':[{'a':1,'b':2}]}"),
172+
WRITER.writeValueAsString(new Bean5476Wrapper(1)));
173+
}
174+
175+
// Test for value parameter
176+
@Test
177+
public void testSpecializedAsIntermediate() throws Exception {
178+
assertEquals(a2q("{'value':{'a':1,'b':2}}"),
179+
WRITER.writeValueAsString(new Bean5476Holder()));
180+
}
181+
182+
// Test for key parameter
183+
@Test
184+
public void testSpecializedKeyAs() throws Exception {
185+
String json = WRITER.writeValueAsString(new MapKeyWrapper());
186+
// Map key serialization depends on how MapKeyAbstract is serialized
187+
// Since it has only getId(), we expect the key to be serialized as just that property
188+
assertTrue(json.contains("\"values\""), "Should contain 'values' field");
189+
}
190+
}

0 commit comments

Comments
 (0)