1

My situation asks for a bit more complex serialisation. I have a class Available (this is a very simplified snippet):

public class Available<T> {
  private T value;
  private boolean available;
  ...
}

So a POJO

class Tmp {
  private Available<Integer> myInt = Available.of(123);
  private Available<Integer> otherInt = Available.clean();
  ...
}

would normally result in

{"myInt":{available:true,value:123},"otherInt":{available:false,value:null}}

However, I want a serialiser to render the same POJO like this:

{"myInt":123}

What I have now:

public class AvailableSerializer extends JsonSerializer<Available<?>> {

  @Override
  public void serialize(Available<?> available, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException, JsonProcessingException {
    if (available != null && available.isAvailable()) {
      jsonGenerator.writeObject(available.getValue());
    }

    // MISSING: nothing at all should be rendered here for the field
  }

  @Override
  public Class<Available<?>> handledType() {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    Class<Available<?>> clazz = (Class) Available.class;
    return clazz;
  }

}

A test

  @Test
  public void testSerialize() throws Exception {
    SimpleModule module = new SimpleModule().addSerializer(new AvailableSerializer());
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerModule(module);

    System.out.println(objectMapper.writeValueAsString(new Tmp()));
  }

outputs

{"myInt":123,"otherInt"}

Can anyone tell me how to do the "MISSING"-stuff? Or if I'm doing it all wrong, how do I do it then?

The restriction I have is that I don't want the developers to add @Json...-annotations all the time to fields of type Available. So the Tmp-class above is an example of what a typical using class should look like. If that's possible...

sjngm
  • 12,423
  • 14
  • 84
  • 114

2 Answers2

2

Include.NON_DEFAULT

If we assume that your clean method is implemented in this way:

class Available<T> {

    public static final Available<Object> EMPTY = clean();

    //....

    @SuppressWarnings("unchecked")
    static <T> Available<T> clean() {
        return (Available<T>) EMPTY;
    }
}

You can set serialisation inclusion to JsonInclude.Include.NON_DEFAULT value and it should skip values set to EMPTY (default) values. See below example:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;

import java.io.IOException;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        SimpleModule module = new SimpleModule();
        module.addSerializer(new AvailableSerializer());

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(module);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);

        System.out.println(objectMapper.writeValueAsString(new Tmp()));
    }
}

class AvailableSerializer extends JsonSerializer<Available<?>> {

    @Override
    public void serialize(Available<?> value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
        jsonGenerator.writeObject(value.getValue());
    }

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Class<Available<?>> handledType() {
        return (Class) Available.class;
    }
}

Above code prints:

{"myInt":123}

Custom BeanPropertyWriter

If you do not want to use Include.NON_DEFAULT you can write your custom BeanPropertyWriter and skip all values you want. See below example:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        SimpleModule module = new SimpleModule();
        module.addSerializer(new AvailableSerializer());
        module.setSerializerModifier(new BeanSerializerModifier() {
            @Override
            public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
                List<BeanPropertyWriter> writers = new ArrayList<>(beanProperties.size());

                for (BeanPropertyWriter writer : beanProperties) {
                    if (writer.getType().getRawClass() == Available.class) {
                        writer = new SkipNotAvailableBeanPropertyWriter(writer);
                    }
                    writers.add(writer);
                }

                return writers;
            }
        });

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(module);

        System.out.println(objectMapper.writeValueAsString(new Tmp()));
    }
}

class AvailableSerializer extends JsonSerializer<Available<?>> {

    @Override
    public void serialize(Available<?> value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException {
        jsonGenerator.writeObject(value.getValue());
    }

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Class<Available<?>> handledType() {
        return (Class) Available.class;
    }
}

class SkipNotAvailableBeanPropertyWriter extends BeanPropertyWriter {

    SkipNotAvailableBeanPropertyWriter(BeanPropertyWriter base) {
        super(base);
    }

    @Override
    public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
        // copier from super.serializeAsField(bean, gen, prov);
        final Object value = (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean, (Object[]) null);
        if (value == null || value instanceof Available && !((Available) value).isAvailable()) {
            return;
        }

        super.serializeAsField(bean, gen, prov);
    }
}

Above code prints:

{"myInt":123}
Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • 1
    What I didn't like was the call to `objectMapper.setSerializationInclusion(...)` as that would cover all classes. Luckily I can do `objectMapper.configOverride(Available.class).setInclude(JsonInclude.Value.construct(JsonInclude.Include.NON_DEFAULT, JsonInclude.Include.ALWAYS));`, which seems to do the same thing, but only for `Available`. So currently it seems as if I don't need the `BeanPropertyWriter`. – sjngm Jul 29 '19 at 06:49
1

After Michał Ziober's answer I had to look for something regarding Include.NON_DEFAULT and the default object and ran into this answer explaining Include.NON_EMPTY that Google didn't return in my first research (thanks Google).

So things become easier, it's now:

public class AvailableSerializer extends JsonSerializer<Available<?>> {

  @Override
  public void serialize(Available<?> available, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException, JsonProcessingException {
    jsonGenerator.writeObject(available.getValue());
  }

  @Override
  public Class<Available<?>> handledType() {
    @SuppressWarnings({ "unchecked", "rawtypes" })
    Class<Available<?>> clazz = (Class) Available.class;
    return clazz;
  }

  @Override
  public boolean isEmpty(SerializerProvider provider, Available<?> value) {
    return value == null || !value.isAvailable();
  }

}

with the test

  @Test
  public void testSerialize() throws Exception {
    SimpleModule module = new SimpleModule().addSerializer(availableSerializer);
    objectMapper.registerModule(module);
    objectMapper.configOverride(Available.class).setInclude(
        // the call comes from JavaDoc of objectMapper.setSerializationInclusion(...)
        JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.ALWAYS));

    Tmp tmp = new Tmp();
    assertThat(objectMapper.writeValueAsString(tmp)).isEqualTo("{\"myInt\":123}");
    tmp.otherInt.setValue(123);
    assertThat(objectMapper.writeValueAsString(tmp)).isEqualTo("{\"myInt\":123,\"otherInt\":123}");
  }

So please, if you upvote my answer please also upvote Michał Ziober's as that's also working with a mildly different approach.

sjngm
  • 12,423
  • 14
  • 84
  • 114