Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One suggestion: could you take MapperConfig<?> as the first parameter? While it is not currently needed, it is something I have had to retrofit for a few methods (see f.ex findCreatorAnnotation()), and in 3.0 every method will be changed to take it. So adding it now reduces conversion work slightly.

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();
Copy link
Member

@cowtowncoder cowtowncoder Oct 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that some additions have been left as abstract, but for maximum backwards-compatibility, let's add "return null;" as implementation just in case.
(this is not meant as an extension point for others but I am guessing it is possible someone might add alt. implementation -- will make pure abstract in 3.0)


/**
* 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method(s) and/or Field(s)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied the comment here from jsonValueAccessors. I'll update them both to match the better language in JsonValue.

*/
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)",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as-key accessors

_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,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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the code does right now; this I'm not sure if this is appropriate.

}
}