jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

`Nulls.AS_EMPTY` returns null in `java.lang.Object`

Open aivinog1 opened this issue 2 years ago • 3 comments

Describe the bug When I'm using JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY, Nulls.AS_EMPTY) for nullable value with type java.lang.Object in a Map, I still getting the null in the deserialized object. I think that problem is that com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer.Vanilla doesn't override com.fasterxml.jackson.databind.JsonDeserializer#getEmptyValue(com.fasterxml.jackson.databind.DeserializationContext). I think that the deserializer for object should return empty object in the getEmptyValue method.

Version information 2.13.2.2

To Reproduce If you have a way to reproduce this with:

  1. Create a JSON, like this:
{
  "a": {
    "first": "second",
    "third": null
  }
}
  1. Create this class to deserialize with:
import java.util.Map;

public class Scratch {
    private Map<String, Object> a;

    public Map<String, Object> getA() {
        return a;
    }

    public void setA(Map<String, Object> a) {
        this.a = a;
    }
}

  1. Create the ObjectMapper and override nullable behavior:
        final ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configOverride(Map.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY, Nulls.AS_EMPTY));
  1. Deserialize this JSON:
        String json = "...";
        final Scratch scratch = objectMapper.readValue(json, Scratch.class);
  1. Verify that in the a object we got a null in the third key:
        final Object third = scratch.getA().get("third");
        assert third != null; // fails

Expected behavior I'm expecting that the empty value of the java.lang.Object is a new Object instance.

aivinog1 avatar May 18 '22 14:05 aivinog1

I am not sure how this can really work unfortunately as there is no obvious "empty" representation of "untyped" value (nominal type of java.lang.Object). I guess theoretically it could be empty Map or empty List. Or maybe scripting language style would be to return empty String. Returning plain new Object() would seem very surprising to me at least.

But... maybe it should be configurable? I could see that working. It would have to be a per-mapping configuration feature, and there'd be lots of wiring. Alternative would be sub-classing of UntypedObjectDeserializer (or Vanilla) but that is very fragile so I would advise against that.

But configurable choice of "empty" value for java.lang.Object is something that I could see working.

cowtowncoder avatar May 19 '22 17:05 cowtowncoder

Thanks, @cowtowncoder for your insights! What do you think about implementing it like this:

  1. We create an interface EmptyValueProvider
  2. Right now I'm thinking about having exactly one method with the signature: Object getEmptyValue(DeserializationContext ctxt, JsonDeserializer<?> deser) throws JsonMappingException
  3. We can start with creating the default implementation - return the value from com.fasterxml.jackson.databind.JsonDeserializer#getNullValue(com.fasterxml.jackson.databind.DeserializationContext)
  4. We add the new method in the com.fasterxml.jackson.databind.annotation.JsonDeserialize with the name emptyValueProvider() and the return type Class<? extends EmptyValueProvider> and default value as the implementation that created in the 3. So a user can just add @JsonDeserialize(emptyValueProvider = MyCoolEmptyValueProvider.class.
  5. I think that the com.fasterxml.jackson.databind.JsonDeserializer should have a private field with the instance of EmptyValueProvider
  6. Wire up all the logic behind this 😅

Why I didn't use the same approach that exists for NullValueProvider you may ask? I think that for this case I should give a user an easy way to implement their implementation like you said: we can think differently about empty values :)

At this point I think that I need to negotiate the API first, then let's think about implementation :)

aivinog1 avatar Jun 01 '22 13:06 aivinog1

Hmmh. Not sure about call flow, use of EmptyValueProvider... The core concept with databind is that serializers and deserializers have the main responsibility for handling, over centralized entities. So I think call really should go through getEmptyValue(...), always. And from that, it would be necessary to make deserializer use something else, possibly provider.

But then again, adding all the logic to pipe in Yet Another Provider might be daunting. I wonder if there was any chance of extending NullValueProvider to also support "empty" (and perhaps even "absent") value configuration. Possibly not, but that what springs to mind.

cowtowncoder avatar Jun 02 '22 18:06 cowtowncoder