问题
I'm serializing instances of
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope=Entity1.class)
public class Entity1 {
private Long id;
@JsonSerialize(converter = ValueMapListConverter.class)
@JsonDeserialize(converter = ValueMapMapConverter.class)
private Map<Entity2, Integer> valueMap = new HashMap<>();
public Entity1() {
}
public Entity1(Long id) {
this.id = id;
}
[getter and setter]
}
and
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope=Entity2.class)
public class Entity2 {
private Long id;
public Entity2() {
}
public Entity2(Long id) {
this.id = id;
}
[getter and setter]
}
with
ObjectMapper objectMapper = new ObjectMapper();
Entity1 entity1 = new Entity1(1l);
Entity2 entity2 = new Entity2(2l);
entity1.getValueMap().put(entity2, 10);
String serialized = objectMapper.writeValueAsString(entity1);
Entity1 deserialized = objectMapper.readValue(serialized, Entity1.class);
assertEquals(entity1,
deserialized);
@JsonSerialize
and @JsonDeserialize
have been added in order to be able to serialize the map with complex key type. The converters are
public class ValueMapMapConverter extends StdConverter<List<Entry<Entity2, Integer>>, Map<Entity2, Integer>> {
@Override
public Map<Entity2, Integer> convert(List<Entry<Entity2, Integer>> value) {
Map<Entity2, Integer> retValue = new HashMap<>();
for(Entry<Entity2, Integer> entry : value) {
retValue.put(entry.getKey(), entry.getValue());
}
return retValue;
}
}
and
public class ValueMapListConverter extends StdConverter<Map<Entity2, Integer>, List<Entry<Entity2, Integer>>> {
@Override
public List<Entry<Entity2, Integer>> convert(Map<Entity2, Integer> value) {
return new LinkedList<>(value.entrySet());
}
}
However, the annotations have no effect since the deserialization still fails due to
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot find a (Map) Key deserializer for type [simple type, class richtercloud.jackson.map.custom.serializer.Entity2]
at [Source: (String)"{"id":1,"valueMap":{"richtercloud.jackson.map.custom.serializer.Entity2@bb":10}}"; line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1451)
at com.fasterxml.jackson.databind.deser.DeserializerCache._handleUnknownKeyDeserializer(DeserializerCache.java:589)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findKeyDeserializer(DeserializerCache.java:168)
at com.fasterxml.jackson.databind.DeserializationContext.findKeyDeserializer(DeserializationContext.java:500)
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.createContextual(MapDeserializer.java:248)
at com.fasterxml.jackson.databind.DeserializationContext.handlePrimaryContextualization(DeserializationContext.java:651)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.resolve(BeanDeserializerBase.java:471)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:293)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:477)
at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:4178)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3997)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2992)
at richtercloud.jackson.map.custom.serializer.TheTest.testSerialization(TheTest.java:29)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)
I took a look into map serialization and am pretty sure I understood the basic concept and expect a key serializer to be unnecessary because the conversion takes place first and the converted output is a list which doesn't need one.
There might be further issues with the serialization of Entry
which I will then overcome by using a different class, eventually my own.
A SSCCE can be found at https://gitlab.com/krichter/jackson-map-custom-serializer.
I'm using Jackson 2.9.4.
回答1:
The problem here is that when you use Map.Entry the key has to be a string, because it gets serialized like {"key": value}
.
You have two options
Your first option if you can serialize your object as string you can use it as the json key.
This is posible in two cases, when the object contains a single field (like the one in your example). e.g.
new SingleFieldObject(2l) // can be serialized as "2"
Or when constains multiple fields that can be represented as string. e.g.
new MultipleFieldObject("John", 23) // can be serialized as "John 23 Years Old"
Now that the custom object can be represented as string you could use either a map or a list of entries.
To use a simple map just use the attribute 'keyUsing' in the annotations, and also you have to define the custom serializer and deserializer.
public class MyKeyDeserializer extends KeyDeserializer {
@Override
public Entity2 deserializeKey(String key,
DeserializationContext ctxt) throws IOException {
return new Entity2(Long.parseLong(key));
}
}
public class MyKeySerializer extends JsonSerializer<Entity2> {
@Override
public void serialize(Entity2 value,
JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeFieldName(value.getId().toString());
}
}
Then you annotate the field with your serializer and deserializer:
@JsonSerialize(keyUsing = MyKeySerializer.class) // no need of converter
@JsonDeserialize(keyUsing = MyKeyDeserializer.class) // no need of converter
private Map<Entity2, Integer> valueMap = new HashMap<>();
Using this object.
Entity1 entity1 = new Entity1(1l);
Entity2 entity2_1 = new Entity2(2l);
Entity2 entity2_2 = new Entity2(3l);
entity1.getValueMap().put(entity2_1, 21);
entity1.getValueMap().put(entity2_2, 22);
A JSON like this is generated
{
"id": 1,
"valueMap": {
"2": 21,
"3": 22
}
}
To use a list you could use the converters in your example, but instead Entity2 you return a String for the key.
public class ValueMapListConverter
extends StdConverter<Map<Entity2, Integer>, List<Entry<String, Integer>>> {
@Override
public List<Entry<String, Integer>> convert(Map<Entity2, Integer> value) {
List<Entry<String, Integer>> result = new ArrayList<>();
for (Entry<Entity2, Integer> entry : value.entrySet()) {
result.add(new SimpleEntry<>(entry.getKey().getId().toString(),
entry.getValue()));
}
return result;
}
}
public class ValueMapMapConverter
extends StdConverter<List<Entry<String, Integer>>, Map<Entity2, Integer>> {
@Override
public Map<Entity2, Integer> convert(List<Entry<String, Integer>> value) {
Map<Entity2, Integer> retValue = new HashMap<>();
for(Entry<String, Integer> entry : value) {
retValue.put(new Entity2(Long.parseLong(entry.getKey())), entry.getValue());
}
return retValue;
}
}
A JSON like this is generated
{
"id": 1,
"valueMap": [
{ "2": 21 },
{ "3": 22 }
]
}
In both cases the value Integer could be a complex object.
Your second option is to use a custom object, again you have multiple options, one object that hold all the fields of the key and the field/fields of the value.
// ... serialization - deserialization of the object
public class CustomObject {
private Long id; // ... all key fields
private int value; // ... all value fields
}
Then you use the converters
public class ValueListMapConverter extends StdConverter<List<CustomObject>, Map<Entity2, Integer>>
and public class ValueMapMapConverter extends StdConverter<Map<Entity2, Integer>, List<CustomObject>>
This generates a JSON like this
{
"id": 1,
"valueMap": [
{ "id": 2, "value": 21 },
{ "id": 3, "value": 22 }
]
}
You could use a map instead a list and use a key, and the rest of the fields of the key object, together with the value fields in a custom object.
// ... serialization - deserialization of the object
public class CustomObject {
// ... rest of the key fields
private int value; // ... all value fields
}
The converters
public class ValueListMapConverter extends StdConverter<Map<Long, CustomObject>, Map<Entity2, Integer>>
and public class ValueMapMapConverter extends StdConverter<Map<Entity2, Integer>, Map<Long, CustomObject>>
This generates a JSON like this
{
"id": 1,
"valueMap": {
"2": { "value": 21 },
"3": { "value": 22 },
}
}
来源:https://stackoverflow.com/questions/49211176/how-to-customly-serialize-or-convert-a-map-property-with-custom-key-type-in-jack