Jackson:Serializing/Deserializing 通过数据绑定在单值数组中的通用对象
Jackson: Serializing/Deserializing generic objects inside single-value arrays via data binding
我所在的团队使用 Jackson 数据绑定来处理发送到 REST API 和从 REST API 发送的 JSON 的序列化和反序列化。 API 广泛使用我们称之为 "key values" 的臭名昭著且难以处理的模式。不幸的是,JSON 的格式不在我们的控制范围内,所以我试图找到一种好的、简单的方法来处理我们这边的序列化和反序列化。
键值总是以下列模式出现:
"key_name":[{
"key":"key_name"
"value":<AN_OBJECT>
}]
值得注意的是,它们总是采用数组的形式,其中包含单个对象,对象内的键 属性 的值始终与外部数组的键相同。该值可以是任何有效的 JSON 值,包括数组或其他对象。键值本身没有值的类型指示符,但对于给定的键值,它始终是相同的类型,因此我们在 DTO class.
中指定它
理想情况下,我想要一种简单、可重复的方法来使用数据绑定对这些属性进行序列化和反序列化。 This question 处理在个人基础上处理非常相似的模式,但由于我们有许多使用该模式的不同 DTO,我想要一些可以重复使用的东西。
例如,我希望能够编写一个如下所示的 DTO class:
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
}
}]
}
我们已经有一个适用于字符串键值的解决方案,但我 运行 在尝试将其概括为适用于非字符串键值对象时遇到了麻烦。
现有的 class 看起来像这样:
@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);
}
}
当试图概括这个 class 时,我 运行 在类型和反序列化方面遇到了麻烦。我尝试在自定义解串器中使用类似这样的东西:
objectMapper.readValue(valueNode.traverse(), objectMapper.getTypeFactory()
.constructParametricType(TypedKeyValue.class, type)));
但我不知道如何确定预期的类型。
除了自定义序列化器和反序列化器,我还研究了使用 "UNWRAP_SINGLE_VALUE_ARRAYS" 反序列化功能。但是,我们正在使用一个共享框架,该框架内部有反序列化器来访问 API,我不想为每个人启用该标志,因为只有我们的团队在处理这个特定的 API .我也找不到只为特定 class 或 属性.
启用该标志的方法
所以,我的问题是,有没有办法做我想做的事?我不一定需要使用自定义序列化器和反序列化器或泛型,但解决方案需要是可以在大量 DTO 中轻松添加和维护的东西,所以我不想有很多每个-class 代码或重复包装器 class 如果我能避免的话。
您不需要自定义 serializer/deserializer。只需使用反映 JSON 结构的 classes 以及 @JsonCreator
annotation on the constructor.
您的 KeyValue
class 可以建模为 KeyValueElement
的列表:
@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 的正确性将取决于您如何实例化此 class。
KeyValueElement
class 可以建模如下:
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
class,无需任何注释。这是一个测试:
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
这个想法是让你的 classes 反映你的 JSON 的结构,这样你的 KeyValue<T>
实际上是一个 List<KeyValueElement<T>>
(只有一个元素) ,其中 KeyValueElement<T>
是一个简单的键值对。
"tricky" 部分是让 Jackson 知道如何反序列化每个 KeyValueElement<T>
。这是 @JsonCreator
注释派上用场并发挥其魔力的时候。
我所在的团队使用 Jackson 数据绑定来处理发送到 REST API 和从 REST API 发送的 JSON 的序列化和反序列化。 API 广泛使用我们称之为 "key values" 的臭名昭著且难以处理的模式。不幸的是,JSON 的格式不在我们的控制范围内,所以我试图找到一种好的、简单的方法来处理我们这边的序列化和反序列化。
键值总是以下列模式出现:
"key_name":[{
"key":"key_name"
"value":<AN_OBJECT>
}]
值得注意的是,它们总是采用数组的形式,其中包含单个对象,对象内的键 属性 的值始终与外部数组的键相同。该值可以是任何有效的 JSON 值,包括数组或其他对象。键值本身没有值的类型指示符,但对于给定的键值,它始终是相同的类型,因此我们在 DTO class.
中指定它理想情况下,我想要一种简单、可重复的方法来使用数据绑定对这些属性进行序列化和反序列化。 This question 处理在个人基础上处理非常相似的模式,但由于我们有许多使用该模式的不同 DTO,我想要一些可以重复使用的东西。
例如,我希望能够编写一个如下所示的 DTO class:
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
}
}]
}
我们已经有一个适用于字符串键值的解决方案,但我 运行 在尝试将其概括为适用于非字符串键值对象时遇到了麻烦。
现有的 class 看起来像这样:
@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);
}
}
当试图概括这个 class 时,我 运行 在类型和反序列化方面遇到了麻烦。我尝试在自定义解串器中使用类似这样的东西:
objectMapper.readValue(valueNode.traverse(), objectMapper.getTypeFactory()
.constructParametricType(TypedKeyValue.class, type)));
但我不知道如何确定预期的类型。
除了自定义序列化器和反序列化器,我还研究了使用 "UNWRAP_SINGLE_VALUE_ARRAYS" 反序列化功能。但是,我们正在使用一个共享框架,该框架内部有反序列化器来访问 API,我不想为每个人启用该标志,因为只有我们的团队在处理这个特定的 API .我也找不到只为特定 class 或 属性.
启用该标志的方法所以,我的问题是,有没有办法做我想做的事?我不一定需要使用自定义序列化器和反序列化器或泛型,但解决方案需要是可以在大量 DTO 中轻松添加和维护的东西,所以我不想有很多每个-class 代码或重复包装器 class 如果我能避免的话。
您不需要自定义 serializer/deserializer。只需使用反映 JSON 结构的 classes 以及 @JsonCreator
annotation on the constructor.
您的 KeyValue
class 可以建模为 KeyValueElement
的列表:
@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 的正确性将取决于您如何实例化此 class。
KeyValueElement
class 可以建模如下:
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
class,无需任何注释。这是一个测试:
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
这个想法是让你的 classes 反映你的 JSON 的结构,这样你的 KeyValue<T>
实际上是一个 List<KeyValueElement<T>>
(只有一个元素) ,其中 KeyValueElement<T>
是一个简单的键值对。
"tricky" 部分是让 Jackson 知道如何反序列化每个 KeyValueElement<T>
。这是 @JsonCreator
注释派上用场并发挥其魔力的时候。