Skip to content

Commit aa0ffc3

Browse files
committed
Adds support for @JsonKey annotation
When serializing the key of a Map, look for a `@JsonKey` annotation. When present (taking priority over `@JsonValue`), skip the StdKey:Serializer and attempt to find a serializer for the inner type. Fixes #2871
1 parent 8b75ed4 commit aa0ffc3

File tree

7 files changed

+180
-9
lines changed

7 files changed

+180
-9
lines changed

src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java

+17
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,23 @@ public PropertyName findNameForSerialization(Annotated a) {
910910
return null;
911911
}
912912

913+
/**
914+
* Method for checking whether given method has an annotation
915+
* that suggests the return value of annotated method
916+
* should be used as "the key" of the object instance; usually
917+
* serialized as a primitive value such as String or number.
918+
*
919+
* @return {@link Boolean#TRUE} if such annotation is found and is not disabled;
920+
* {@link Boolean#FALSE} if disabled annotation (block) is found (to indicate
921+
* accessor is definitely NOT to be used "as value"); or `null` if no
922+
* information found.
923+
*
924+
* @since TODO
925+
*/
926+
public Boolean hasAsKey(Annotated a) {
927+
return null;
928+
}
929+
913930
/**
914931
* Method for checking whether given method has an annotation
915932
* that suggests that the return value of annotated method

src/main/java/com/fasterxml/jackson/databind/BeanDescription.java

+11
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ public boolean isNonStaticInnerClass() {
173173
/**********************************************************
174174
*/
175175

176+
/**
177+
* Method for locating accessor (readable field, or "getter" method)
178+
* that has
179+
* {@link com.fasterxml.jackson.annotation.JsonKey} annotation,
180+
* if any. If multiple ones are found,
181+
* an error is reported by throwing {@link IllegalArgumentException}
182+
*
183+
* @since TODO
184+
*/
185+
public abstract AnnotatedMember findJsonKeyAccessor();
186+
176187
/**
177188
* Method for locating accessor (readable field, or "getter" method)
178189
* that has

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

+6
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ public List<BeanPropertyDefinition> findProperties() {
239239
return _properties();
240240
}
241241

242+
@Override
243+
public AnnotatedMember findJsonKeyAccessor() {
244+
return (_propCollector == null) ? null
245+
: _propCollector.getJsonKeyAccessor();
246+
}
247+
242248
@Override
243249
@Deprecated // since 2.9
244250
public AnnotatedMethod findJsonValueMethod() {

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

+9
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,15 @@ public PropertyName findNameForSerialization(Annotated a)
10501050
return null;
10511051
}
10521052

1053+
@Override
1054+
public Boolean hasAsKey(Annotated a) {
1055+
JsonKey ann = _findAnnotation(a, JsonKey.class);
1056+
if (ann == null) {
1057+
return null;
1058+
}
1059+
return ann.value();
1060+
}
1061+
10531062
@Override // since 2.9
10541063
public Boolean hasAsValue(Annotated a) {
10551064
JsonValue ann = _findAnnotation(a, JsonValue.class);

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

+37
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public class POJOPropertiesCollector
112112

113113
protected LinkedList<AnnotatedMember> _anySetterField;
114114

115+
/**
116+
* Method(s) annotated with 'JsonKey' annotation
117+
*/
118+
protected LinkedList<AnnotatedMember> _jsonKeyAccessors;
119+
115120
/**
116121
* Method(s) marked with 'JsonValue' annotation
117122
*<p>
@@ -194,6 +199,23 @@ public Map<Object, AnnotatedMember> getInjectables() {
194199
return _injectables;
195200
}
196201

202+
public AnnotatedMember getJsonKeyAccessor() {
203+
if (!_collected) {
204+
collectAll();
205+
}
206+
// If @JsonKey defined, must have a single one
207+
if (_jsonKeyAccessors != null) {
208+
if (_jsonKeyAccessors.size() > 1) {
209+
reportProblem("Multiple 'as-value' properties defined (%s vs %s)",
210+
_jsonKeyAccessors.get(0),
211+
_jsonKeyAccessors.get(1));
212+
}
213+
// otherwise we won't greatly care
214+
return _jsonKeyAccessors.get(0);
215+
}
216+
return null;
217+
}
218+
197219
/**
198220
* @since 2.9
199221
*/
@@ -391,6 +413,13 @@ protected void _addFields(Map<String, POJOPropertyBuilder> props)
391413
final boolean transientAsIgnoral = _config.isEnabled(MapperFeature.PROPAGATE_TRANSIENT_MARKER);
392414

393415
for (AnnotatedField f : _classDef.fields()) {
416+
// @JsonKey?
417+
if (Boolean.TRUE.equals(ai.hasAsKey(f))) {
418+
if (_jsonKeyAccessors == null) {
419+
_jsonKeyAccessors = new LinkedList<>();
420+
}
421+
_jsonKeyAccessors.add(f);
422+
}
394423
// @JsonValue?
395424
if (Boolean.TRUE.equals(ai.hasAsValue(f))) {
396425
if (_jsonValueAccessors == null) {
@@ -593,6 +622,14 @@ protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props,
593622
_anyGetters.add(m);
594623
return;
595624
}
625+
// @JsonKey?
626+
if (Boolean.TRUE.equals(ai.hasAsKey(m))) {
627+
if (_jsonKeyAccessors == null) {
628+
_jsonKeyAccessors = new LinkedList<>();
629+
}
630+
_jsonKeyAccessors.add(m);
631+
return;
632+
}
596633
// @JsonValue?
597634
if (Boolean.TRUE.equals(ai.hasAsValue(m))) {
598635
if (_jsonValueAccessors == null) {

src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java

+21-9
Original file line numberDiff line numberDiff line change
@@ -227,18 +227,30 @@ public JsonSerializer<Object> createKeySerializer(SerializerProvider ctxt,
227227
ser = StdKeySerializers.getStdKeySerializer(config, keyType.getRawClass(), false);
228228
// As per [databind#47], also need to support @JsonValue
229229
if (ser == null) {
230-
AnnotatedMember am = beanDesc.findJsonValueAccessor();
231-
if (am != null) {
232-
final Class<?> rawType = am.getRawType();
233-
JsonSerializer<?> delegate = StdKeySerializers.getStdKeySerializer(config,
234-
rawType, true);
230+
AnnotatedMember keyAm = beanDesc.findJsonKeyAccessor();
231+
if (keyAm != null) {
232+
final Class<?> rawType = keyAm.getRawType();
233+
JsonSerializer<?> delegate = createKeySerializer(ctxt, config.constructType(rawType), null);
235234
if (config.canOverrideAccessModifiers()) {
236-
ClassUtil.checkAndFixAccess(am.getMember(),
235+
ClassUtil.checkAndFixAccess(keyAm.getMember(),
237236
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
238237
}
239-
ser = new JsonValueSerializer(am, delegate);
240-
} else {
241-
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
238+
ser = new JsonValueSerializer(keyAm, delegate);
239+
}
240+
if (ser == null) {
241+
AnnotatedMember am = beanDesc.findJsonValueAccessor();
242+
if (am != null) {
243+
final Class<?> rawType = am.getRawType();
244+
JsonSerializer<?> delegate = StdKeySerializers.getStdKeySerializer(config,
245+
rawType, true);
246+
if (config.canOverrideAccessModifiers()) {
247+
ClassUtil.checkAndFixAccess(am.getMember(),
248+
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
249+
}
250+
ser = new JsonValueSerializer(am, delegate);
251+
} else {
252+
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
253+
}
242254
}
243255
}
244256
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.fasterxml.jackson.databind.jsontype;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
6+
import com.fasterxml.jackson.annotation.JsonKey;
7+
import com.fasterxml.jackson.annotation.JsonValue;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import org.junit.Assert;
10+
import org.junit.Ignore;
11+
import org.junit.Test;
12+
13+
public class MapSerializingTest {
14+
class Inner {
15+
@JsonKey
16+
String key;
17+
18+
@JsonValue
19+
String value;
20+
21+
Inner(String key, String value) {
22+
this.key = key;
23+
this.value = value;
24+
}
25+
26+
public String toString() {
27+
return "Inner(" + this.key + "," + this.value + ")";
28+
}
29+
30+
}
31+
32+
class Outer {
33+
@JsonKey
34+
@JsonValue
35+
Inner inner;
36+
37+
Outer(Inner inner) {
38+
this.inner = inner;
39+
}
40+
41+
}
42+
43+
class NoKeyOuter {
44+
@JsonValue
45+
Inner inner;
46+
47+
NoKeyOuter(Inner inner) {
48+
this.inner = inner;
49+
}
50+
}
51+
52+
@Ignore
53+
@Test
54+
public void testClassAsKey() throws Exception {
55+
ObjectMapper mapper = new ObjectMapper();
56+
Outer outer = new Outer(new Inner("innerKey", "innerValue"));
57+
Map<Outer, String> map = Collections.singletonMap(outer, "value");
58+
String actual = mapper.writeValueAsString(map);
59+
Assert.assertEquals("{\"innerKey\":\"value\"}", actual);
60+
}
61+
62+
@Ignore
63+
@Test
64+
public void testClassAsValue() throws Exception {
65+
ObjectMapper mapper = new ObjectMapper();
66+
Map<String, Outer> mapA = Collections.singletonMap("key", new Outer(new Inner("innerKey", "innerValue")));
67+
String actual = mapper.writeValueAsString(mapA);
68+
Assert.assertEquals("{\"key\":\"innerValue\"}", actual);
69+
}
70+
71+
@Ignore
72+
@Test
73+
public void testNoKeyOuter() throws Exception {
74+
ObjectMapper mapper = new ObjectMapper();
75+
Map<String, NoKeyOuter> mapA = Collections.singletonMap("key", new NoKeyOuter(new Inner("innerKey", "innerValue")));
76+
String actual = mapper.writeValueAsString(mapA);
77+
Assert.assertEquals("{\"key\":\"innerValue\"}", actual);
78+
}
79+
}

0 commit comments

Comments
 (0)