Jackson Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

This tutorial is going to illustrate how we can use Jackson to only serialize a field if it meets a specific, custom criteria.

For example, say we only want to serialize an integer value if it's positive – and we want to skip it entirely if it's not.

If you want to dig deeper and learn other cool things you can do with the Jackson 2 – head on over to the main Jackson tutorial.

2. Use Jackson Filter to Control the Serialization Process

First, we need to define the filter on our entity, using the @JsonFilter annotation:

@JsonFilter("myFilter")
public class MyDto {
    private int intValue;

    public MyDto() {
        super();
    }

    public int getIntValue() {
        return intValue;
    }

    public void setIntValue(int intValue) {
        this.intValue = intValue;
    }
}

Then, we need to define our custom PropertyFilter:

PropertyFilter theFilter = new SimpleBeanPropertyFilter() {
   @Override
   public void serializeAsField
    (Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
     throws Exception {
      if (include(writer)) {
         if (!writer.getName().equals("intValue")) {
            writer.serializeAsField(pojo, jgen, provider);
            return;
         }
         int intValue = ((MyDtoWithFilter) pojo).getIntValue();
         if (intValue >= 0) {
            writer.serializeAsField(pojo, jgen, provider);
         }
      } else if (!jgen.canOmitFields()) { // since 2.3
         writer.serializeAsOmittedField(pojo, jgen, provider);
      }
   }
   @Override
   protected boolean include(BeanPropertyWriter writer) {
      return true;
   }
   @Override
   protected boolean include(PropertyWriter writer) {
      return true;
   }
};

This filter contains the actual logic deciding if the intValue field is going to be serialized or not, based on its value.

Next, we hook this filter into the ObjectMapper and we serialize an entity:

FilterProvider filters = new SimpleFilterProvider().addFilter("myFilter", theFilter);
MyDto dtoObject = new MyDto();
dtoObject.setIntValue(-1);

ObjectMapper mapper = new ObjectMapper();
String dtoAsString = mapper.writer(filters).writeValueAsString(dtoObject);

And finally, we can check that the intValue field is indeed not part of the marshalled JSON output:

assertThat(dtoAsString, not(containsString("intValue")));

3. Skip Objects Conditionally

Now – let's discuss how to skip objects while serializing based on property value. We will skip all objects where property hidden is true:

3.1. Hidable Classes

First, let's take a look at our Hidable Interface:

@JsonIgnoreProperties("hidden")
public interface Hidable {
    boolean isHidden();
}

And we have two simple classes implementing this interface Person, Address:

Person Class:

public class Person implements Hidable {
    private String name;
    private Address address;
    private boolean hidden;
}

And Address Class:

public class Address implements Hidable {
    private String city;
    private String country;
    private boolean hidden;
}

Note: We used @JsonIgnoreProperties(“hidden”) to make sure hidden property itself is not included in JSON

3.2. Custom Serializer

Next – here is our custom serializer:

public class HidableSerializer extends JsonSerializer<Hidable> {

    private JsonSerializer<Object> defaultSerializer;

    public HidableSerializer(JsonSerializer<Object> serializer) {
        defaultSerializer = serializer;
    }

    @Override
    public void serialize(Hidable value, JsonGenerator jgen, SerializerProvider provider)
      throws IOException, JsonProcessingException {
        if (value.isHidden())
            return;
        defaultSerializer.serialize(value, jgen, provider);
    }

    @Override
    public boolean isEmpty(SerializerProvider provider, Hidable value) {
        return (value == null || value.isHidden());
    }
}

Note that:

  • When the object will not be skipped, we delegate the serialization to the default injected serializer.
  • We overridden the method isEmpty() – to make sure that in case of Hidable object is a property, property name is also excluded from JSON.

3.3. Using BeanSerializerModifier

Finally, we will need to use BeanSerializerModifier to inject default serializer in our custom HidableSerializer – as follows:

ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_EMPTY);
mapper.registerModule(new SimpleModule() {
    @Override
    public void setupModule(SetupContext context) {
        super.setupModule(context);
        context.addBeanSerializerModifier(new BeanSerializerModifier() {
            @Override
            public JsonSerializer<?> modifySerializer(
              SerializationConfig config, BeanDescription desc, JsonSerializer<?> serializer) {
                if (Hidable.class.isAssignableFrom(desc.getBeanClass())) {
                    return new HidableSerializer((JsonSerializer<Object>) serializer);
                }
                return serializer;
            }
        });
    }
});

3.4. Sample Output

Here is a simple serialization example:

Address ad1 = new Address("tokyo", "jp", true);
Address ad2 = new Address("london", "uk", false);
Address ad3 = new Address("ny", "usa", false);
Person p1 = new Person("john", ad1, false);
Person p2 = new Person("tom", ad2, true);
Person p3 = new Person("adam", ad3, false);

System.out.println(mapper.writeValueAsString(Arrays.asList(p1, p2, p3)));

And the output is:

[
    {
        "name":"john"
    },
    {
        "name":"adam",
        "address":{
            "city":"ny",
            "country":"usa"
        }
    }
]

3.5. Test

Finally – here is few test cases:

First case, nothing is hidden:

@Test
public void whenNotHidden_thenCorrect() throws JsonProcessingException {
    Address ad = new Address("ny", "usa", false);
    Person person = new Person("john", ad, false);
    String result = mapper.writeValueAsString(person);

    assertTrue(result.contains("name"));
    assertTrue(result.contains("john"));
    assertTrue(result.contains("address"));
    assertTrue(result.contains("usa"));
}

Next, only address is hidden:

@Test
public void whenAddressHidden_thenCorrect() throws JsonProcessingException {
    Address ad = new Address("ny", "usa", true);
    Person person = new Person("john", ad, false);
    String result = mapper.writeValueAsString(person);

    assertTrue(result.contains("name"));
    assertTrue(result.contains("john"));
    assertFalse(result.contains("address"));
    assertFalse(result.contains("usa"));
}

Now, entire person is hidden:

@Test
public void whenAllHidden_thenCorrect() throws JsonProcessingException {
    Address ad = new Address("ny", "usa", false);
    Person person = new Person("john", ad, true);
    String result = mapper.writeValueAsString(person);

    assertTrue(result.length() == 0);
}

4. Conclusion

This type of advanced filtering is incredibly powerful and allows very flexible customization of the json when serializing complex objects with Jackson.

A more flexible but also more complex alternative would be using a fully custom serializer to control the JSON output – so if this solution isn't flexible enough, it may be worth looking into that.

The implementation of all these examples and code snippets can be found over on GitHub – it's a Maven-based project, so it should be easy to import and run as it is.

Jackson bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
10 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!