【问题标题】:Jackson: Serializing/Deserializing generic objects inside single-value arrays via data bindingJackson:通过数据绑定对单值数组中的通用对象进行序列化/反序列化
【发布时间】:2015-07-15 15:55:48
【问题描述】:

我所在的团队使用 Jackson 数据绑定来处理发送到 REST API 和从 REST API 发送的 JSON 的序列化和反序列化。 API 广泛使用一种臭名昭著且难以处理的模式,我们称之为“关键值”。不幸的是,JSON 的格式超出了我们的控制范围,所以我试图找到一种简单的方法来处理我们对它们的序列化和反序列化。

键值始终以以下模式出现:

"key_name":[{
    "key":"key_name"
    "value":<AN_OBJECT>
}]

值得注意的是,它们总是采用数组的形式,其中包含单个对象,并且对象内的 key 属性的值始终与外部数组的 key 相同。该值可以是任何有效的 JSON 值,包括数组或其他对象。键值本身没有值的类型指示符,但对于给定的键值,它始终是相同的类型,因此我们在 DTO 类中指定它。

理想情况下,我想要一种简单、可重复的方式来使用数据绑定来序列化和反序列化这些属性。 This question 处理单独处理非常相似的模式,但由于我们有许多不同的 DTO 使用该模式,我想要可以重复使用的东西。

例如,我希望能够编写一个看起来像这样的 DTO 类:

public class ContactInfo {
    private String id;
    private KeyValue<String> email;
    private KeyValue<String> phone_number;
    private KeyValue<Address> address;

    // Getters and setters

    public static class Address {
        private String street;
        private String city;
        private String state;
        private String zip;

        // Getters and setters
    }
}    

并让它能够通过如下所示的数据绑定 JSON 进行序列化和反序列化:

{
    "id":"123456",
    "email":[{
        "key":"email",
        "value":"fakeperson@example.com"
    }],
    "phone_number":[{
        "key":"phone_number",
        "value":"1-555-555-1234"
    }],
    "address":[{
        "key":"address",
        "value":{
            "street":"123 Main Street",
            "city":"New York City",
            "state":"NY",
            "zip":"12345
        }
    }]
}

我们已经有一个适用于字符串键值的解决方案,但是当我试图将其推广到非字符串键值对象时遇到了麻烦。


现有的类如下所示:

@JsonSerialize(using = KeyValueSerializer.class)
@JsonDeserialize(using = KeyValueDeserializer.class)
public class KeyValue {

    private String key;
    private String value;

    // Omitted getters and setters...
}

public class KeyValueSerializer extends JsonSerializer<KeyValue> {

    @Override
    public void serialize(KeyValue keyValue, JsonGenerator jgen, SerializerProvider provider)
        throws IOException, JsonProcessingException {
        jgen.writeStartArray();
        jgen.writeStartObject();
        jgen.writeStringField("label", keyValue.getKey());
        jgen.writeStringField("value", keyValue.getValue());
        jgen.writeEndObject();
        jgen.writeEndArray();
    }
}

public class KeyValueDeserializer extends StdDeserializer<KeyValue> {

    protected KeyValueDeserializer() {
        super(KeyValue.class);
    }

    @Override
    public KeyValue deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
        JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        String value = node.findValue("value").asText();
        String key = node.findValue("key").asText();
        return new KeyValue(key, value);
    }

}

在尝试泛化这个类时,我遇到了类型和反序列化的问题。我尝试在自定义反序列化器中使用类似的东西:

objectMapper.readValue(valueNode.traverse(), objectMapper.getTypeFactory()
    .constructParametricType(TypedKeyValue.class, type)));

但我不知道如何确定预期的类型应该是什么。

除了自定义序列化器和反序列化器,我还研究了使用“UNWRAP_SINGLE_VALUE_ARRAYS”反序列化功能。但是,我们正在使用一个共享框架,其中包含反序列化器来访问 API,我不想为每个人启用该标志,因为只有我们的团队正在使用这个特定的 API。我也找不到只为特定类或属性启用该标志的方法。


所以,我的问题是,有没有办法做我想做的事?我不一定需要使用自定义序列化器和反序列化器或泛型,但解决方案需要是可以在大量 DTO 中轻松添加和维护的东西,因此我不希望每个类都有很多如果可以避免的话,代码或重复的包装类。

【问题讨论】:

    标签: java serialization jackson deserialization


    【解决方案1】:

    您不需要自定义序列化器/反序列化器。只需使用反映 JSON 结构的类以及 @JsonCreator annotation on the constructor

    您的KeyValue 类可以建模为KeyValueElements 的列表:

    @SuppressWarnings("serial")
    public class KeyValue<T>
        extends LinkedList<KeyValueElement<T>> {
    
        public KeyValue() {
        }
    
        public KeyValue(String key, T value) {
            Map<String, Object> map = new HashMap<>(1);
            map.put(key, value);
            KeyValueElement<T> elem = new KeyValueElement<>(map);
            this.add(elem);
        }
    }
    

    我提供了 2 个构造函数:一个用于 Jackson(一个没有参数),另一个用于简化键值对的创建。 生成的 JSON 的正确性取决于您如何实例化此类。

    KeyValueElement 类可以建模如下:

    public class KeyValueElement<T> {
    
        private String key;
    
        private T value;
    
        @SuppressWarnings("unchecked")
        @JsonCreator
        public KeyValueElement(Map<String, Object> delegate) {
            this.key = (String) delegate.get("key");
            this.value = (T) delegate.get("value");
        }
    
        public String getKey() {
            return this.key;
        }
    
        public void setKey(String key) {
            this.key = key;
        }
    
        public T getValue() {
            return this.value;
        }
    
        public void setValue(T value) {
            this.value = value;
        }
    }
    

    然后,只需使用您的 ContactInfo 类,无需任何注释。这是一个测试:

    ObjectMapper mapper = new ObjectMapper().setPropertyNamingStrategy(
        PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
    
    String json = "{\"id\":\"123456\",\"email\":[{\"key\":\"email\",\"value\":\"fakeperson@example.com\"}],\"phone_number\":[{\"key\":\"phone_number\",\"value\":\"1-555-555-1234\"}],\"address\":[{\"key\":\"address\",\"value\":{\"street\":\"123 Main Street\",\"city\":\"New York City\",\"state\":\"NY\",\"zip\":\"12345\"}}]}";
    
    ContactInfo contactInfo = mapper.readValue(json, ContactInfo.class);
    
    String serialized = mapper.writeValueAsString(contactInfo);
    
    System.out.println(json.equals(serialized)); // true
    

    这个想法是让你的类反映你的 JSON 结构,这样你的 KeyValue&lt;T&gt; 实际上是一个 List&lt;KeyValueElement&lt;T&gt;&gt;(只有一个元素),KeyValueElement&lt;T&gt; 是一个简单的键值对。

    “棘手”的部分是让杰克逊知道如何反序列化每个KeyValueElement&lt;T&gt;。这就是@JsonCreator 注释派上用场并发挥其魔力的时候。

    【讨论】:

    • 感谢您的回复。未经修改的解决方案并不是我所需要的,但它给了我一个很好的起点。我需要的是抽象出 KeyValue 的“列表”部分(因为列表中总是只有一个值,使它们变得毫无用处)。我能够通过添加转发方法来访问内部元素并使用 @JsonIgnore 标记它们来修改您的类
    • @Pausbrak 我很高兴我的回答很有用,至少作为一个起点。你的问题可不简单……
    • 编辑了我之前的评论
    猜你喜欢
    • 1970-01-01
    • 2011-07-10
    • 2014-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-03
    • 1970-01-01
    • 2021-10-05
    相关资源
    最近更新 更多