如何不使用 jackson objectMapper 编写 Option.None (并读取它)?

How to not write Option.None with jackson objectMapper (and read it)?

我使用 jackson ObjectMapper 序列化和反序列化我的一些数据,这些数据具有 javaslang Option 类型的字段。我使用 JavaslangModule(和 Jdk8Module)。当它写入 json 时,Option.None 值字段被写为 null.

为了减少 json 大小并在以后添加新字段时提供一些简单的向后兼容性,我想要的是:

  1. 值为Option.None的字段根本就不写,
  2. 缺少json个Option类型数据模型对应的字段,读取后设为Option.None

=> 这可能吗?如何实现?

注意: 我认为 not-writing/removing null json 字段可以解决 (1)。可能吗?然后,阅读它是否有效(即,如果 json 中缺少具有选项值的模型字段,请将其设置为 None?

我找到了一个适用于 immuatble (lombok @Value) 模型的解决方案:

  1. 使用不写入 Option.None 的 mixIn 在所有 Object 上添加过滤器(参见下面的 "the solution")
  2. 我现有的 ObjectMapper(带有 JavaslangModule)已经将 None 设置为选项字段,而相应的 json 条目缺失

代码

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import javaslang.control.Option;
import javaslang.jackson.datatype.JavaslangModule;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.junit.Test;

import java.io.IOException;
import java.lang.reflect.Field;

public class JsonModelAndSerialization {

  // Write to Json
  // =============

  private static ObjectMapper objectMapper = new ObjectMapper()
      .registerModule(new Jdk8Module())
      .registerModule(new JavaslangModule())

      // not required but provide forward compatibility on new field
      .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);


  static String write(Object data) throws JsonProcessingException {
    SimpleBeanPropertyFilter filter = new NoneOptionPropertyFilter();
    objectMapper.addMixIn(Object.class, NoneOptionFilter.class);
    final SimpleFilterProvider filters = new SimpleFilterProvider().setDefaultFilter(filter);
    ObjectWriter writer = objectMapper.writer(filters);

    return writer.writeValueAsString(data);
  }

  // Filter classes
  // ==============

  @JsonFilter("Filter None")
  private static class NoneOptionFilter {}

  private static class NoneOptionPropertyFilter extends SimpleBeanPropertyFilter {
    @Override
    public void serializeAsField(
        Object pojo, JsonGenerator jgen,
        SerializerProvider provider, PropertyWriter writer) throws Exception{
      Field field = pojo.getClass().getDeclaredField(writer.getName());
      if(field.getType().equals(Option.class)){
        field.setAccessible(true);
        Option<?> value = (Option<?>) field.get(pojo);
        if(value.isEmpty()) return;
      }
      super.serializeAsField(pojo, jgen, provider, writer);
    }
  }

  // Usage example
  // =============

  // **important note**
  // For @Value deserialization, a lombok config file should be added
  // in the source folder of the model class definition
  // with content:
  //    lombok.anyConstructor.addConstructorProperties = true

  @Value
  @AllArgsConstructor(onConstructor_={@JsonCreator})
  public static class StringInt {
    private int intValue;
    private Option<String> stringValue;
  }

  @Value
  @AllArgsConstructor(onConstructor_={@JsonCreator})
  public static class StringIntPair {
    private StringInt item1;
    private StringInt item2;
  }

  @Test
  public void readWriteMyClass() throws IOException {
    StringIntPair myClass = new StringIntPair(
      new StringInt(6 * 9, Option.some("foo")),
      new StringInt( 42, Option.none()));

    String json = write(myClass);
    // {"item1":{"intValue":54,"stringValue":"foo"},"item2":{"intValue":42}}

    StringIntPair myClass2 = objectMapper.readValue(json, StringIntPair.class);

    assertThat(myClass2).isEqualTo(myClass);
  }
}

优点:

  • Option.None 时减小 json 的大小(因此在模型中添加选项字段在不使用时不会占用大小)
  • 稍后在模型中添加选项类型的字段时提供向后读取兼容性(默认为 None

劣势:

  • 无法区分具有 None 字段值的正确数据和错误地忘记了该字段的不正确数据。我觉得这个还是可以接受的。

幸运的是有一个更简单的解决方案。

1) 在您的 ObjectMapper 配置中,将序列化包含设置为仅包含非缺失字段:

  @Bean
  public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModules(vavr());
    objectMapper.setSerializationInclusion(NON_ABSENT);

    return objectMapper;
  }

2) 将可选字段的默认值设置为 Option.none:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Foo {
  private Option<String> bar = Option.none(); // If the JSON field is null or not present, the field will be initialized with none
}

就是这样!

更好的消息是它适用于所有 Iterables,而不仅仅是 Option。特别是它也适用于 Vavr List 类型!