【问题标题】:How to customly serialize or convert a Map property with custom key type in Jackson JSON (de-)serialization?如何在 Jackson JSON(反)序列化中自定义序列化或转换具有自定义键类型的 Map 属性?
【发布时间】:2018-08-19 01:16:45
【问题描述】:

我正在序列化的实例

@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]
}

@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]
}

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@JsonDeserialize 以便能够序列化具有复杂键类型的映射。转换器是

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;
    }
}

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());
    }
}

但是,注释没有效果,因为反序列化仍然失败,因为

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)

我研究了 map 序列化,我很确定我理解了基本概念,并认为不需要密钥序列化器,因为转换首先发生,转换后的输出是一个不需要的列表。

Entry 的序列化可能会出现更多问题,然后我将通过使用不同的类(最终是我自己的类)来解决这些问题。

可以在https://gitlab.com/krichter/jackson-map-custom-serializer找到SSCCE。

我使用的是 Jackson 2.9.4。

【问题讨论】:

    标签: java json jackson jackson-databind


    【解决方案1】:

    这里的问题是,当您使用Map.Entry 时,键必须是字符串,因为它会像{"key": value} 一样被序列化。


    您有两种选择


    您的第一个选项如果您可以将对象序列化为字符串,则可以将其用作 json 键。

    当对象包含单个字段(如您的示例中的字段)时,这在两种情况下是可能的。例如

    new SingleFieldObject(2l) // can be serialized as "2"
    

    或者当包含多个可以表示为字符串的字段时。例如

    new MultipleFieldObject("John", 23) // can be serialized as "John 23 Years Old"
    

    现在自定义对象可以表示为字符串,您可以使用地图或条目列表。

    要使用简单的映射,只需在注释中使用属性“keyUsing”,您还必须定义自定义序列化器和反序列化器。

    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());
        }
    }
    

    然后你用你的序列化器和反序列化器注释字段:

    @JsonSerialize(keyUsing = MyKeySerializer.class) // no need of converter
    @JsonDeserialize(keyUsing = MyKeyDeserializer.class) // no need of converter
    private Map<Entity2, Integer> valueMap = new HashMap<>();
    

    使用这个对象。

    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);
    

    生成这样的 JSON

    {
        "id": 1,
        "valueMap": {
            "2": 21,
            "3": 22
        }
    }
    

    要使用列表,您可以在示例中使用转换器,但 Entity2 您返回一个字符串作为键。

    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;
        }
    }
    

    生成这样的 JSON

    {
        "id": 1,
        "valueMap": [
            { "2": 21 },
            { "3": 22 }
        ]
    }
    

    在这两种情况下,值 Integer 都可能是一个复杂的对象。


    您的第二个选项是使用自定义对象,同样您有多个选项,一个对象包含键的所有字段和值的字段。

    // ... serialization - deserialization of the object
    public class CustomObject {
        private Long id; // ... all key fields
        private int value; // ... all value fields
    }
    

    然后你使用转换器 public class ValueListMapConverter extends StdConverter&lt;List&lt;CustomObject&gt;, Map&lt;Entity2, Integer&gt;&gt;public class ValueMapMapConverter extends StdConverter&lt;Map&lt;Entity2, Integer&gt;, List&lt;CustomObject&gt;&gt;

    这会生成这样的 JSON

    {
        "id": 1,
        "valueMap": [
            { "id": 2, "value": 21 },
            { "id": 3, "value": 22 }
        ]
    }
    

    您可以使用映射而不是列表并使用键、键对象的其余字段以及自定义对象中的值字段。

    // ... serialization - deserialization of the object
    public class CustomObject {
        // ... rest of the key fields
        private int value; // ... all value fields
    }
    

    转换器 public class ValueListMapConverter extends StdConverter&lt;Map&lt;Long, CustomObject&gt;, Map&lt;Entity2, Integer&gt;&gt;public class ValueMapMapConverter extends StdConverter&lt;Map&lt;Entity2, Integer&gt;, Map&lt;Long, CustomObject&gt;&gt;

    这会生成这样的 JSON

    {
        "id": 1,
        "valueMap": {
            "2": { "value": 21 },
            "3": { "value": 22 },
        }
    }
    

    【讨论】:

    • 感谢您的详细解答。我尝试了一些基于转换器的方法,但由于不同的原因而失败,考虑到 afaiu Jackson 2.9 在序列化支持方面存在大量问题这一事实,这似乎微不足道,例如github.com/FasterXML/jackson-databind/issues/1419 这会导致与某些序列化实际上根本不支持或不工作的问题完全无关的故障。我在 SSCCE 的提交 2ea6797 中创建了一个有效的自定义序列化程序(这将是第三个选项),尽管必须注意类型擦除问题,如 所述
    • stackoverflow.com/questions/49213606/…。我接受您的回答,因为它指出了“密钥必须是字符串”的关键部分。如果您合并第三种方法并删除第一种方法,我会很高兴,因为它不适用于任意复杂的对象。您知道如何让地图序列化程序使用@JsonIdentityInfo 提供的 ID 引用吗?
    • 好的,我可以删除第一种方法。但是,合并第三个是什么意思?你的意思是扩展/改进。现在还有什么特定的失败吗?我看到测试运行良好。参考@JsonIdentityInfo,至少我不知道序列化程序如何使用 id(如果我理解正确,我们说的是JsonIdentityInfo#property)。当然,您可以使用反射并从注释中获取您想要的所有信息,但我不确定这是否是您想要的。
    • 我没有测试你的方法,因为我的第三个解决方案对我来说效果很好,但是因为问题和答案适用于每个人,所以有它很好(是的,我的意思是扩展你的答案,如果你想)。读取 id 引用的第二件事是使用 {f1: {...object1}, f2: [1: {...object2}]} 而不是 {f1: {...object1}, f2: [{...object1}: {...object2}]} 其中 1object1 的 id,但这基本上是一个不同的问题。
    • 分支“继承”中的解决方案是第二种方法选项1。同样,每个人的答案都适用于第一种方法,有人可能会使用它,所以我认为只是扩展第二种方法会很好。至于评论中你的json,我没听懂,你使用的是数组中的键,显然json键不能是对象,但无论如何它都超出了这个范围。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-02
    • 2011-04-12
    • 2013-01-18
    • 2014-08-02
    • 2019-08-10
    • 2013-10-10
    • 2016-01-29
    相关资源
    最近更新 更多