1

I use jackson ObjectMapper to serialize and deserialize some data of mine, which have fields of javaslang Option type. I use JavaslangModule (and Jdk8Module). And when it write the json, Option.None value fields are written as null.

To reduce the json size and provide some simple backward compatibility when later adding new fields, what I want is that:

  1. fields with Option.None value are simply not written,
  2. missing json fields that correspond to data model of Option type, be set to Option.None upon reading

=> Is that possible, and how?

Note: I think that not-writing/removing null json fields would solve (1). Is it possible? And then, would reading it works (i.e. if model field with Option value is missing in the json, set it None?

Juh_
  • 14,628
  • 8
  • 59
  • 92

2 Answers2

1

Luckily there is a much simpler solution.

1) In your ObjectMapper configuration, set serialization inclusion to only include non absent field:

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

    return objectMapper;
  }

2) Set the default value of your optional fields to 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
}

That's it!

And the even better news is that it works for all Iterables, not just for Option. In particular it also works for Vavr List type!

Sir4ur0n
  • 1,753
  • 1
  • 13
  • 24
  • 1
    Actually I prefer [my solution](https://stackoverflow.com/a/58979079/1206998) below because it doesn't require to add defaults to the model definitions which doesn't fit with the use of pure @Value immutable classes. But then, this solution is simpler to code. – Juh_ Dec 03 '19 at 13:13
0

I found a solution that works with immuatble (lombok @Value) models:

  1. add a filter on all Object using mixIn that doesn't write Option.None (see "the solution" below)
  2. my existing ObjectMapper (with JavaslangModule) is already setting None to Option field when the corresponding json entry is missing

The code

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

The advantages:

  • reduce size of json when having Option.None (thus adding Option fields in the model doesn't cost size when not used)
  • it provides backward reading compatibility when later adding field with Option type in the model (which will default to None)

The disadvantage:

  • It is not possible to differentiate correct data with None field value and incorrect data where the field has erroneously been forgotten. I think this is quite acceptable.
Juh_
  • 14,628
  • 8
  • 59
  • 92