Skip to content

Commit

Permalink
Adds support for @JsonKey annotation (#2905)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Anusien authored Nov 2, 2020
1 parent 3de2de0 commit ecc9bfe
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,23 @@ public PropertyName findNameForSerialization(Annotated a) {
return null;
}

/**
* Method for checking whether given method has an annotation
* that suggests the return value of annotated method
* should be used as "the key" of the object instance; usually
* serialized as a primitive value such as String or number.
*
* @return {@link Boolean#TRUE} if such annotation is found and is not disabled;
* {@link Boolean#FALSE} if disabled annotation (block) is found (to indicate
* accessor is definitely NOT to be used "as value"); or `null` if no
* information found.
*
* @since TODO
*/
public Boolean hasAsKey(MapperConfig<?> config, Annotated a) {
return null;
}

/**
* Method for checking whether given method has an annotation
* that suggests that the return value of annotated method
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/BeanDescription.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,19 @@ public boolean isNonStaticInnerClass() {
/**********************************************************
*/

/**
* Method for locating accessor (readable field, or "getter" method)
* that has
* {@link com.fasterxml.jackson.annotation.JsonKey} annotation,
* if any. If multiple ones are found,
* an error is reported by throwing {@link IllegalArgumentException}
*
* @since TODO
*/
public AnnotatedMember findJsonKeyAccessor() {
return null;
}

/**
* Method for locating accessor (readable field, or "getter" method)
* that has
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ public List<BeanPropertyDefinition> findProperties() {
return _properties();
}

@Override
public AnnotatedMember findJsonKeyAccessor() {
return (_propCollector == null) ? null
: _propCollector.getJsonKeyAccessor();
}

@Override
@Deprecated // since 2.9
public AnnotatedMethod findJsonValueMethod() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,15 @@ public PropertyName findNameForSerialization(Annotated a)
return null;
}

@Override
public Boolean hasAsKey(MapperConfig<?> config, Annotated a) {
JsonKey ann = _findAnnotation(a, JsonKey.class);
if (ann == null) {
return null;
}
return ann.value();
}

@Override // since 2.9
public Boolean hasAsValue(Annotated a) {
JsonValue ann = _findAnnotation(a, JsonValue.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;

import com.fasterxml.jackson.annotation.JsonKey;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.*;

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

/**
* Method(s) marked with 'JsonValue' annotation
* Accessors (field or "getter" method annotated with {@link JsonKey}
*/
protected LinkedList<AnnotatedMember> _jsonKeyAccessors;

/**
*Accessors (field or "getter" method) annotated with {@link JsonValue}
*<p>
* NOTE: before 2.9, was `AnnotatedMethod`; with 2.9 allows fields too
*/
Expand Down Expand Up @@ -192,6 +199,23 @@ public Map<Object, AnnotatedMember> getInjectables() {
return _injectables;
}

public AnnotatedMember getJsonKeyAccessor() {
if (!_collected) {
collectAll();
}
// If @JsonKey defined, must have a single one
if (_jsonKeyAccessors != null) {
if (_jsonKeyAccessors.size() > 1) {
reportProblem("Multiple 'as-key' properties defined (%s vs %s)",
_jsonKeyAccessors.get(0),
_jsonKeyAccessors.get(1));
}
// otherwise we won't greatly care
return _jsonKeyAccessors.get(0);
}
return null;
}

/**
* @since 2.9
*/
Expand Down Expand Up @@ -421,6 +445,13 @@ protected void _addFields(Map<String, POJOPropertyBuilder> props)
final boolean transientAsIgnoral = _config.isEnabled(MapperFeature.PROPAGATE_TRANSIENT_MARKER);

for (AnnotatedField f : _classDef.fields()) {
// @JsonKey?
if (Boolean.TRUE.equals(ai.hasAsKey(_config, f))) {
if (_jsonKeyAccessors == null) {
_jsonKeyAccessors = new LinkedList<>();
}
_jsonKeyAccessors.add(f);
}
// @JsonValue?
if (Boolean.TRUE.equals(ai.hasAsValue(f))) {
if (_jsonValueAccessors == null) {
Expand Down Expand Up @@ -646,6 +677,14 @@ protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props,
_anyGetters.add(m);
return;
}
// @JsonKey?
if (Boolean.TRUE.equals(ai.hasAsKey(_config, m))) {
if (_jsonKeyAccessors == null) {
_jsonKeyAccessors = new LinkedList<>();
}
_jsonKeyAccessors.add(m);
return;
}
// @JsonValue?
if (Boolean.TRUE.equals(ai.hasAsValue(m))) {
if (_jsonValueAccessors == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,19 +228,31 @@ public JsonSerializer<Object> createKeySerializer(SerializerProvider ctxt,
ser = StdKeySerializers.getStdKeySerializer(config, keyType.getRawClass(), false);
// As per [databind#47], also need to support @JsonValue
if (ser == null) {
AnnotatedMember am = beanDesc.findJsonValueAccessor();
if (am != null) {
final Class<?> rawType = am.getRawType();
JsonSerializer<?> delegate = StdKeySerializers.getStdKeySerializer(config,
rawType, true);
AnnotatedMember keyAm = beanDesc.findJsonKeyAccessor();
if (keyAm != null) {
final Class<?> rawType = keyAm.getRawType();
JsonSerializer<?> delegate = createKeySerializer(ctxt, config.constructType(rawType), null);
if (config.canOverrideAccessModifiers()) {
ClassUtil.checkAndFixAccess(am.getMember(),
ClassUtil.checkAndFixAccess(keyAm.getMember(),
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
}
// null -> no TypeSerializer for key-serializer use case
ser = new JsonValueSerializer(am, null, delegate);
} else {
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
ser = new JsonValueSerializer(keyAm, null, delegate);
}
if (ser == null) {
AnnotatedMember am = beanDesc.findJsonValueAccessor();
if (am != null) {
final Class<?> rawType = am.getRawType();
JsonSerializer<?> delegate = StdKeySerializers.getStdKeySerializer(config,
rawType, true);
if (config.canOverrideAccessModifiers()) {
ClassUtil.checkAndFixAccess(am.getMember(),
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
}
ser = new JsonValueSerializer(am, null, delegate);
} else {
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.fasterxml.jackson.databind.jsontype;

import java.util.Collections;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonKey;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

public class MapSerializingTest {
class Inner {
@JsonKey
String key;

@JsonValue
String value;

Inner(String key, String value) {
this.key = key;
this.value = value;
}

public String toString() {
return "Inner(" + this.key + "," + this.value + ")";
}

}

class Outer {
@JsonKey
@JsonValue
Inner inner;

Outer(Inner inner) {
this.inner = inner;
}

}

class NoKeyOuter {
@JsonValue
Inner inner;

NoKeyOuter(Inner inner) {
this.inner = inner;
}
}

@Test
public void testClassAsKey() throws Exception {
ObjectMapper mapper = new ObjectMapper();
Outer outer = new Outer(new Inner("innerKey", "innerValue"));
Map<Outer, String> map = Collections.singletonMap(outer, "value");
String actual = mapper.writeValueAsString(map);
Assert.assertEquals("{\"innerKey\":\"value\"}", actual);
}

@Test
public void testClassAsValue() throws Exception {
ObjectMapper mapper = new ObjectMapper();
Map<String, Outer> mapA = Collections.singletonMap("key", new Outer(new Inner("innerKey", "innerValue")));
String actual = mapper.writeValueAsString(mapA);
Assert.assertEquals("{\"key\":\"innerValue\"}", actual);
}

@Test
public void testNoKeyOuter() throws Exception {
ObjectMapper mapper = new ObjectMapper();
Map<String, NoKeyOuter> mapA = Collections.singletonMap("key", new NoKeyOuter(new Inner("innerKey", "innerValue")));
String actual = mapper.writeValueAsString(mapA);
Assert.assertEquals("{\"key\":\"innerValue\"}", actual);
}
}

0 comments on commit ecc9bfe

Please sign in to comment.