Skip to content

Commit ecc9bfe

Browse files
authored
Adds support for @JsonKey annotation (#2905)
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 3de2de0 commit ecc9bfe

File tree

7 files changed

+182
-10
lines changed

7 files changed

+182
-10
lines changed

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

+17
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(MapperConfig<?> config, 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

+13
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,19 @@ 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 AnnotatedMember findJsonKeyAccessor() {
186+
return null;
187+
}
188+
176189
/**
177190
* Method for locating accessor (readable field, or "getter" method)
178191
* 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
@@ -1071,6 +1071,15 @@ public PropertyName findNameForSerialization(Annotated a)
10711071
return null;
10721072
}
10731073

1074+
@Override
1075+
public Boolean hasAsKey(MapperConfig<?> config, 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

+40-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.fasterxml.jackson.annotation.JacksonInject;
77
import com.fasterxml.jackson.annotation.JsonCreator;
88

9+
import com.fasterxml.jackson.annotation.JsonKey;
10+
import com.fasterxml.jackson.annotation.JsonValue;
911
import com.fasterxml.jackson.databind.*;
1012

1113
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
@@ -112,7 +114,12 @@ public class POJOPropertiesCollector
112114
protected LinkedList<AnnotatedMember> _anySetterField;
113115

114116
/**
115-
* Method(s) marked with 'JsonValue' annotation
117+
* Accessors (field or "getter" method annotated with {@link JsonKey}
118+
*/
119+
protected LinkedList<AnnotatedMember> _jsonKeyAccessors;
120+
121+
/**
122+
*Accessors (field or "getter" method) annotated with {@link JsonValue}
116123
*<p>
117124
* NOTE: before 2.9, was `AnnotatedMethod`; with 2.9 allows fields too
118125
*/
@@ -192,6 +199,23 @@ public Map<Object, AnnotatedMember> getInjectables() {
192199
return _injectables;
193200
}
194201

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-key' 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+
195219
/**
196220
* @since 2.9
197221
*/
@@ -421,6 +445,13 @@ protected void _addFields(Map<String, POJOPropertyBuilder> props)
421445
final boolean transientAsIgnoral = _config.isEnabled(MapperFeature.PROPAGATE_TRANSIENT_MARKER);
422446

423447
for (AnnotatedField f : _classDef.fields()) {
448+
// @JsonKey?
449+
if (Boolean.TRUE.equals(ai.hasAsKey(_config, f))) {
450+
if (_jsonKeyAccessors == null) {
451+
_jsonKeyAccessors = new LinkedList<>();
452+
}
453+
_jsonKeyAccessors.add(f);
454+
}
424455
// @JsonValue?
425456
if (Boolean.TRUE.equals(ai.hasAsValue(f))) {
426457
if (_jsonValueAccessors == null) {
@@ -646,6 +677,14 @@ protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props,
646677
_anyGetters.add(m);
647678
return;
648679
}
680+
// @JsonKey?
681+
if (Boolean.TRUE.equals(ai.hasAsKey(_config, m))) {
682+
if (_jsonKeyAccessors == null) {
683+
_jsonKeyAccessors = new LinkedList<>();
684+
}
685+
_jsonKeyAccessors.add(m);
686+
return;
687+
}
649688
// @JsonValue?
650689
if (Boolean.TRUE.equals(ai.hasAsValue(m))) {
651690
if (_jsonValueAccessors == null) {

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

+21-9
Original file line numberDiff line numberDiff line change
@@ -228,19 +228,31 @@ 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
}
240239
// null -> no TypeSerializer for key-serializer use case
241-
ser = new JsonValueSerializer(am, null, delegate);
242-
} else {
243-
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
240+
ser = new JsonValueSerializer(keyAm, null, delegate);
241+
}
242+
if (ser == null) {
243+
AnnotatedMember am = beanDesc.findJsonValueAccessor();
244+
if (am != null) {
245+
final Class<?> rawType = am.getRawType();
246+
JsonSerializer<?> delegate = StdKeySerializers.getStdKeySerializer(config,
247+
rawType, true);
248+
if (config.canOverrideAccessModifiers()) {
249+
ClassUtil.checkAndFixAccess(am.getMember(),
250+
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
251+
}
252+
ser = new JsonValueSerializer(am, null, delegate);
253+
} else {
254+
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
255+
}
244256
}
245257
}
246258
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
@Test
53+
public void testClassAsKey() throws Exception {
54+
ObjectMapper mapper = new ObjectMapper();
55+
Outer outer = new Outer(new Inner("innerKey", "innerValue"));
56+
Map<Outer, String> map = Collections.singletonMap(outer, "value");
57+
String actual = mapper.writeValueAsString(map);
58+
Assert.assertEquals("{\"innerKey\":\"value\"}", actual);
59+
}
60+
61+
@Test
62+
public void testClassAsValue() throws Exception {
63+
ObjectMapper mapper = new ObjectMapper();
64+
Map<String, Outer> mapA = Collections.singletonMap("key", new Outer(new Inner("innerKey", "innerValue")));
65+
String actual = mapper.writeValueAsString(mapA);
66+
Assert.assertEquals("{\"key\":\"innerValue\"}", actual);
67+
}
68+
69+
@Test
70+
public void testNoKeyOuter() throws Exception {
71+
ObjectMapper mapper = new ObjectMapper();
72+
Map<String, NoKeyOuter> mapA = Collections.singletonMap("key", new NoKeyOuter(new Inner("innerKey", "innerValue")));
73+
String actual = mapper.writeValueAsString(mapA);
74+
Assert.assertEquals("{\"key\":\"innerValue\"}", actual);
75+
}
76+
}

0 commit comments

Comments
 (0)