jackson-databind
jackson-databind copied to clipboard
`Nulls.AS_EMPTY` returns null in `java.lang.Object`
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:
- Create a JSON, like this:
{
"a": {
"first": "second",
"third": null
}
}
- 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;
}
}
- Create the
ObjectMapperand override nullable behavior:
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configOverride(Map.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY, Nulls.AS_EMPTY));
- Deserialize this JSON:
String json = "...";
final Scratch scratch = objectMapper.readValue(json, Scratch.class);
- Verify that in the
aobject we got anullin thethirdkey:
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.
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.
Thanks, @cowtowncoder for your insights! What do you think about implementing it like this:
- We create an interface
EmptyValueProvider - Right now I'm thinking about having exactly one method with the signature:
Object getEmptyValue(DeserializationContext ctxt, JsonDeserializer<?> deser) throws JsonMappingException - We can start with creating the default implementation - return the value from
com.fasterxml.jackson.databind.JsonDeserializer#getNullValue(com.fasterxml.jackson.databind.DeserializationContext) - We add the new method in the
com.fasterxml.jackson.databind.annotation.JsonDeserializewith the nameemptyValueProvider()and the return typeClass<? extends EmptyValueProvider>and default value as the implementation that created in the 3. So a user can just add@JsonDeserialize(emptyValueProvider = MyCoolEmptyValueProvider.class. - I think that the
com.fasterxml.jackson.databind.JsonDeserializershould have a private field with the instance ofEmptyValueProvider - 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 :)
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.