How ignore empty json objects like "car":{}, that cause empty pojos after deserialisation with jackson

Viewed 765

I have a rest service that consume json from an Angular UI and also from other rest clients. The data based on a complex structure of entities ~50 that are stored in a database with ~50 tables. The problem are the optional OneToOne relations, because Angular send the optional objects as empty definitions like "car": {},. The spring data repository saves them as empty entries and I got a Json response like "car": {"id": 545234, "version": 0} back. I found no Jackson annotation to ignore empty objects, only empty or null properties.

The Employee Entity has the following form:

@Entity
public class Employee {
  @Id 
  @GeneratedValue
  private Long id;
  
  @Version
  private Long version;

  private String name;

  @OneToOne(cascade = CascadeType.ALL)
  @JoinColumn(name = "car_id")
  @JsonManagedReference
  private Car car;

  .
  .   Desk, getters and setters
  . 
}

and the other side of the OneToOne Reference

@Entity
public class Car{
  @Id
  @GeneratedValue
  private Long id;

  @Version
  private Long version;

  private String name;

  @OneToOne(fetch = FetchType.LAZY, mappedBy = "employee")
  @JsonBackReference
  private Employee employee;


  .
  .   getters and setters
  . 
}

For example, I send this to my service as post operation

{
  "name": "ACME",
      .
      .
      .
  "employee": {
    "name": "worker 1",
    "car": {},
    "desk": {
      floor: 3,
      number: 4,
      phone: 444
    }
      .
      .
      .
  },
  "addresses": [],
  "building": {},
      .
      .
      .
}

and I got as response the saved data

{
  "id": 34534,
  "version": 0,
  "name": "ACME",
      .
      .
      .
  "employee": {
    "id": 34535,
    "version":0,
    "name": "worker 1",
    "car": {"id": 34536, "version": 0},
    "desk": {
      "id": 34538,
      "version":0,
      "floor": 3,
      "number": 4,
      "phone": 444
    }
      .
      .
      .
  },
  "addresses": [],
  "building": {"id": 34539, "version": 0},
      .
      .
      .
}

As seen in the response I got empty table rows with an id, a version, many null values and empty strings, because when I save (persist) the main deserialized company class, the other entity are also saved, because the are annotated as cascading.

I found many examples like Do not include empty object to Jackson , with a concrete pojo and a concrete deserializer that are working, but every entity needs his own Deserializer. This causes many work for the current entities and the new ones in the future (only the optional entities).

I tried the folowing, I write a BeanDeserializerModifier and try to wrap an own deserializer over the standard beandeserializer:

    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() {
        @Override
        public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config,
                                                             BeanDescription beanDesc,
                                                             List<BeanPropertyDefinition> propDefs) {
            logger.debug("update properties, beandesc: {}", beanDesc.getBeanClass().getSimpleName());
            return super.updateProperties(config, beanDesc, propDefs);
        }

        @Override
        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                                                      BeanDescription beanDesc,
                                                      JsonDeserializer<?> deserializer) {
            
            logger.debug("modify deserializer {}",beanDesc.getBeanClass().getSimpleName());
            // This fails:
           // return new DeserializationWrapper(deserializer, beanDesc);
            return deserializer; // This works, but it is the standard behavior
        }
    });

And here is the wrapper (and the mistake):

public class DeserializationWrapper extends JsonDeserializer<Object> {
private static final Logger logger = LoggerFactory.getLogger( DeserializationWrapper.class );

    private final JsonDeserializer<?> deserializer;
    private final BeanDescription beanDesc;

    public DeserializationWrapper(JsonDeserializer<?> deserializer, BeanDescription beanDesc) {
        this.deserializer = deserializer;
        this.beanDesc = beanDesc;
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        logger.debug("deserialize in wrapper {} ",beanDesc.getBeanClass().getSimpleName());
        final Object deserialized = deserializer.deserialize(p, ctxt);
        ObjectCodec codec = p.getCodec();
        JsonNode node = codec.readTree(p);
        
         // some logig that not work
         // here. The Idea is to detect with the json parser that the node is empty.
         // If it is empty I will return null here and not the deserialized pojo

        return deserialized;
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {
        logger.debug("deserializer - method 2");
        intoValue = deserializer.deserialize(p, ctxt);
        return intoValue;
    }

    @Override
    public boolean isCachable() {
        return deserializer.isCachable();
    }
  .
  .     I try to wrap the calls to the deserializer
  .

The Deserialization Wrapper does not work and crash after the first call with an exception com.fasterxml.jackson.databind.exc.MismatchedInputException: No _valueDeserializer assigned at [Source: (PushbackInputStream); line: 2, column: 11] (through reference chain: ... Company["name"])

My question: is there a way to extend the behavior of the working standard deserializer in the way, that the deserializer detect while parsing, that the current jsonNode is empty and return null instead the empty class instance? Perhaps my Idea is wrong and there is a completely other solution?

Solving it on the Angular UI side is no option. We use Jackson 2.9.5.

1 Answers

Using BeanDeserializerModifier with custom deserialiser is a good idea. You need to improve only a deserialiser implementation. In your example problem is with these lines:

final Object deserialized = deserializer.deserialize(p, ctxt); //1.
ObjectCodec codec = p.getCodec(); //2.
JsonNode node = codec.readTree(p); //3.

Line 1. reads JSON Object. In lines 2. and 3. you want to create a JsonNode but empty JSON Object was already read in line 1.. Next two lines will try to read rest of payload as JsonNode.

Jackson by default uses BeanDeserializer to deserialise regular POJO classes. We can extend this class and provide our own implementation. In version 2.10.1 deserialise method looks like this:

@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
    // common case first
    if (p.isExpectedStartObjectToken()) {
        if (_vanillaProcessing) {
            return vanillaDeserialize(p, ctxt, p.nextToken());
        }
        // 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
        //    what it is, including "expected behavior".
        p.nextToken();
        if (_objectIdReader != null) {
            return deserializeWithObjectId(p, ctxt);
        }
        return deserializeFromObject(p, ctxt);
    }
    return _deserializeOther(p, ctxt, p.getCurrentToken());
}

In most cases, where there is no special treatment needed vanillaDeserialize method will be invoked. Let's look on it:

private final Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
    final Object bean = _valueInstantiator.createUsingDefault(ctxt);
    // [databind#631]: Assign current value, to be accessible by custom serializers
    p.setCurrentValue(bean);
    if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
        String propName = p.getCurrentName();
        do {
            p.nextToken();
            SettableBeanProperty prop = _beanProperties.find(propName);

            if (prop != null) { // normal case
                try {
                    prop.deserializeAndSet(p, ctxt, bean);
                } catch (Exception e) {
                    wrapAndThrow(e, bean, propName, ctxt);
                }
                continue;
            }
            handleUnknownVanilla(p, ctxt, bean, propName);
        } while ((propName = p.nextFieldName()) != null);
    }
    return bean;
}

As you can see, it does almost everything what we want, except it creates new object even for empty JSON Object. It checks whether field exists just after it creates new objects. One line too far. Unfortunately, this method is private and we can not override it. Let's copy it to our class and modify a little bit:

class EmptyObjectIsNullBeanDeserializer extends BeanDeserializer {

    EmptyObjectIsNullBeanDeserializer(BeanDeserializerBase src) {
        super(src);
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        if (_vanillaProcessing) {
            return vanillaDeserialize(p, ctxt);
        }

        return super.deserialize(p, ctxt);
    }

    private Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        p.nextToken();
        if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
            final Object bean = _valueInstantiator.createUsingDefault(ctxt);
            // [databind#631]: Assign current value, to be accessible by custom serializers
            p.setCurrentValue(bean);
            String propName = p.getCurrentName();
            do {
                p.nextToken();
                SettableBeanProperty prop = _beanProperties.find(propName);

                if (prop != null) { // normal case
                    try {
                        prop.deserializeAndSet(p, ctxt, bean);
                    } catch (Exception e) {
                        wrapAndThrow(e, bean, propName, ctxt);
                    }
                    continue;
                }
                handleUnknownVanilla(p, ctxt, bean, propName);
            } while ((propName = p.nextFieldName()) != null);
            return bean;
        }
        return null;
    }
}

You can register it like below:

class EmptyObjectIsNullBeanDeserializerModifier extends BeanDeserializerModifier {
    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        if (beanDesc.getBeanClass() == Car.class) { //TODO: change this condition
            return new EmptyObjectIsNullBeanDeserializer((BeanDeserializerBase) deserializer);
        }
        return super.modifyDeserializer(config, beanDesc, deserializer);
    }
}

Simple POC:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.Data;

import java.io.File;
import java.io.IOException;

public class EmptyObjectIsNullApp {

    public static void main(String[] args) throws IOException {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        SimpleModule emptyObjectIsNullModule = new SimpleModule();
        emptyObjectIsNullModule.setDeserializerModifier(new EmptyObjectIsNullBeanDeserializerModifier());

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(emptyObjectIsNullModule);

        Wrapper wrapper = mapper.readValue(jsonFile, Wrapper.class);
        System.out.println(wrapper);
    }
}

@Data
class Wrapper {
    private Car car;
}

@Data
class Car {
    private int id;
}

For "empty object" JSON payload:

{
  "car": {}
}

Above code prints:

Wrapper(car=null)

For JSON payload with some fields:

{
  "car": {
    "id": 1
  }
}

Above code prints:

Wrapper(car=Car(id=1))
Related