Skip to content

Commit e9a11ff

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 960f4f6 commit e9a11ff

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,23 @@ public PropertyName findNameForSerialization(Annotated a) {
967967
return null;
968968
}
969969

970+
/**
971+
* Method for checking whether given method has an annotation
972+
* that suggests the return value of annotated method
973+
* should be used as "the key" of the object instance; usually
974+
* serialized as a primitive value such as String or number.
975+
*
976+
* @return {@link Boolean#TRUE} if such annotation is found and is not disabled;
977+
* {@link Boolean#FALSE} if disabled annotation (block) is found (to indicate
978+
* accessor is definitely NOT to be used "as value"); or `null` if no
979+
* information found.
980+
*
981+
* @since TODO
982+
*/
983+
public Boolean hasAsKey(Annotated a) {
984+
return null;
985+
}
986+
970987
/**
971988
* Method for checking whether given method has an annotation
972989
* that suggests that the return value of annotated method

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

Lines changed: 11 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,15 @@ public PropertyName findNameForSerialization(Annotated a)
10711071
return null;
10721072
}
10731073

1074+
@Override
1075+
public Boolean hasAsKey(Annotated a) {
1076+
JsonKey ann = _findAnnotation(a, JsonKey.class);
1077+
if (ann == null) {
1078+
return null;
1079+
}
1080+
return ann.value();
1081+
}
1082+
10741083
@Override // since 2.9
10751084
public Boolean hasAsValue(Annotated a) {
10761085
JsonValue ann = _findAnnotation(a, JsonValue.class);

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ public class POJOPropertiesCollector
106106

107107
protected LinkedList<AnnotatedMember> _anySetterField;
108108

109+
/**
110+
* Method(s) annotated with 'JsonKey' annotation
111+
*/
112+
protected LinkedList<AnnotatedMember> _jsonKeyAccessors;
113+
109114
/**
110115
* Method(s) marked with 'JsonValue' annotation
111116
*<p>
@@ -187,6 +192,23 @@ public Map<Object, AnnotatedMember> getInjectables() {
187192
return _injectables;
188193
}
189194

195+
public AnnotatedMember getJsonKeyAccessor() {
196+
if (!_collected) {
197+
collectAll();
198+
}
199+
// If @JsonKey defined, must have a single one
200+
if (_jsonKeyAccessors != null) {
201+
if (_jsonKeyAccessors.size() > 1) {
202+
reportProblem("Multiple 'as-value' properties defined (%s vs %s)",
203+
_jsonKeyAccessors.get(0),
204+
_jsonKeyAccessors.get(1));
205+
}
206+
// otherwise we won't greatly care
207+
return _jsonKeyAccessors.get(0);
208+
}
209+
return null;
210+
}
211+
190212
/**
191213
* @since 2.9
192214
*/
@@ -384,6 +406,13 @@ protected void _addFields(Map<String, POJOPropertyBuilder> props)
384406
final boolean transientAsIgnoral = _config.isEnabled(MapperFeature.PROPAGATE_TRANSIENT_MARKER);
385407

386408
for (AnnotatedField f : _classDef.fields()) {
409+
// @JsonKey?
410+
if (Boolean.TRUE.equals(ai.hasAsKey(f))) {
411+
if (_jsonKeyAccessors == null) {
412+
_jsonKeyAccessors = new LinkedList<>();
413+
}
414+
_jsonKeyAccessors.add(f);
415+
}
387416
// @JsonValue?
388417
if (Boolean.TRUE.equals(ai.hasAsValue(f))) {
389418
if (_jsonValueAccessors == null) {
@@ -596,6 +625,14 @@ protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props,
596625
_anyGetters.add(m);
597626
return;
598627
}
628+
// @JsonKey?
629+
if (Boolean.TRUE.equals(ai.hasAsKey(m))) {
630+
if (_jsonKeyAccessors == null) {
631+
_jsonKeyAccessors = new LinkedList<>();
632+
}
633+
_jsonKeyAccessors.add(m);
634+
return;
635+
}
599636
// @JsonValue?
600637
if (Boolean.TRUE.equals(ai.hasAsValue(m))) {
601638
if (_jsonValueAccessors == null) {

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -228,18 +228,30 @@ public JsonSerializer<Object> createKeySerializer(SerializerProvider ctxt,
228228
ser = StdKeySerializers.getStdKeySerializer(config, keyType.getRawClass(), false);
229229
// As per [databind#47], also need to support @JsonValue
230230
if (ser == null) {
231-
AnnotatedMember am = beanDesc.findJsonValueAccessor();
232-
if (am != null) {
233-
final Class<?> rawType = am.getRawType();
234-
JsonSerializer<?> delegate = StdKeySerializers.getStdKeySerializer(config,
235-
rawType, true);
231+
AnnotatedMember keyAm = beanDesc.findJsonKeyAccessor();
232+
if (keyAm != null) {
233+
final Class<?> rawType = keyAm.getRawType();
234+
JsonSerializer<?> delegate = createKeySerializer(ctxt, config.constructType(rawType), null);
236235
if (config.canOverrideAccessModifiers()) {
237-
ClassUtil.checkAndFixAccess(am.getMember(),
236+
ClassUtil.checkAndFixAccess(keyAm.getMember(),
238237
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
239238
}
240-
ser = new JsonValueSerializer(am, delegate);
241-
} else {
242-
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
239+
ser = new JsonValueSerializer(keyAm, delegate);
240+
}
241+
if (ser == null) {
242+
AnnotatedMember am = beanDesc.findJsonValueAccessor();
243+
if (am != null) {
244+
final Class<?> rawType = am.getRawType();
245+
JsonSerializer<?> delegate = StdKeySerializers.getStdKeySerializer(config,
246+
rawType, true);
247+
if (config.canOverrideAccessModifiers()) {
248+
ClassUtil.checkAndFixAccess(am.getMember(),
249+
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
250+
}
251+
ser = new JsonValueSerializer(am, delegate);
252+
} else {
253+
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
254+
}
243255
}
244256
}
245257
}
Lines changed: 79 additions & 0 deletions
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)