Search code examples
javajsonjacksonguavajackson-modules

Jackson and the Guava datatype module: Ignore missing fields, but serialize explicit nulls


I am storing test data for a RESTful endpoint in json. I will be deserializing the test data into POJOs to pass into REST Assured for testing. Underneath the hood, REST Assured serializes the POJOs using Jackson when constructing request objects.

I am using the Guava datatype module to distinguish between fields which are present in the json but set to null and those which are not present at all. When json is deserialized to a POJO using jackson with the guava datatype module for Optional fields, fields which are missing from the json are set to null in the POJO, whereas those fields which are present in the json with value set explicitly to null are set to Optional.absent().

When the POJO is serialized by Rest Assured (with the help of Jackson under the hood) to construct the HTTP Reguest object, I want the 'missing' fields (null in the POJO) to be ignored, but those fields which are present, but set explicitly to null (Optional.absent() in the POJO) to be serialized. I cannot use the @JsonInclude(Include.NON_NULL) annotation because it excludes both cases, due to the way that the Serializer for Guava datatypes in the Guava datatype module handles them. I want a way to override this behavior while preserving my ability to use the module. I'm not sure how to do it.

Here is example of what I am talking about:

public class Item {
    private Optional<Integer> id;
    private Optional<String> name;
    private Optional<String> description;
}

{ "name":null, "description":"Something" }

The above POJO would be instantiated like this once the json String above is deserialized:

Item: 
    id = null 
    name = Optional.<String>absent()
    description = "Something"

When the POJO is serialized back to json, I want the output to be what it is in test data:

{ "name":null, "description":"Something" }

However, what I get is this:

{ "id":null, "name":null, "description":"Something" }

If I use the @JsonInclude(Include.NON_NULL), I get this:

{ "description":"Something" }

UPDATE:

So, I basically hacked the module to do what I wanted. Here are the alterations I made:

In GuavaOptionalSerializer, I altered isEmpty(Optional<?> value) to return true only if the value was null and not if is either null or absent.

@Override
public boolean isEmpty(Optional<?> value) {
    return (value == null);
}

And in GuavaOptionalBeanPropertyWriter I altered the serializeAsField method to first handle suppressing empty values and then to handle null values:

@Override
public void serializeAsField(Object bean, JsonGenerator jgen, SerializerProvider prov) throws Exception
{                   
    // and then see if we must suppress certain values (default, empty)
    if (_suppressableValue != null) {       
        super.serializeAsField(bean, jgen, prov);
        return;
    }

    if (_nullSerializer == null) {
        Object value = get(bean);   
        if (Optional.absent().equals(value)) {
             return;
         }      
    }

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

Now, if I want to include the absent fields (explicitly set to null) and exclude the fields which are simple 'missing', I use the @JsonInclude(Include.NON_EMPTY) annotation.


Solution

  • Unfortunately, looking at the source for the serializer, I don't see there being an out-of-the-box way to do what you're looking for. Optional.absent() just delegates to the default null-handling behavior, which will respect the @JsonInclude annotation.

    This is reasonable, because the whole motivation of Optional<> is that you shouldn't assign null to an Optional<> reference.

    However, you can always write your own serializer to do this for you. The implementation would be much like the linked GuavaOptionalSerializer, but you would need to alter this function:

    @Override
    public void serialize(Optional<?> value, JsonGenerator jgen, SerializerProvider provider)
            throws IOException, JsonGenerationException {
        if(value.isPresent()){
            provider.defaultSerializeValue(value.get(), jgen);
        } else{
            // provider.defaultSerializeNull(jgen);
            // force writing null here instead of using default
        }
    }