Skip to content

Commit

Permalink
Adds support for @JsonKey annotation
Browse files Browse the repository at this point in the history
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 FasterXML#2871
  • Loading branch information
Anusien committed Oct 26, 2020
1 parent 960f4f6 commit e9a11ff
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 9 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(Annotated a) {
return null;
}

/**
* Method for checking whether given method has an annotation
* that suggests that the return value of annotated method
Expand Down
11 changes: 11 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,17 @@ 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 abstract AnnotatedMember findJsonKeyAccessor();

/**
* 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(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 @@ -106,6 +106,11 @@ public class POJOPropertiesCollector

protected LinkedList<AnnotatedMember> _anySetterField;

/**
* Method(s) annotated with 'JsonKey' annotation
*/
protected LinkedList<AnnotatedMember> _jsonKeyAccessors;

/**
* Method(s) marked with 'JsonValue' annotation
*<p>
Expand Down Expand Up @@ -187,6 +192,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-value' 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 @@ -384,6 +406,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(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 @@ -596,6 +625,14 @@ protected void _addGetterMethod(Map<String, POJOPropertyBuilder> props,
_anyGetters.add(m);
return;
}
// @JsonKey?
if (Boolean.TRUE.equals(ai.hasAsKey(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,18 +228,30 @@ 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));
}
ser = new JsonValueSerializer(am, delegate);
} else {
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
ser = new JsonValueSerializer(keyAm, 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, delegate);
} else {
ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass());
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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;
}
}

@Ignore
@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);
}

@Ignore
@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);
}

@Ignore
@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 e9a11ff

Please sign in to comment.