Describe your Issue
Hello, Jackson devs! I have been trying to do this with Jackson but have been unable to: I want it to fail on deserialization if a field is null or absent, but only if the field is not annotated with org.springframework.lang.Nullable
. Example:
My POJO:
public record TestRecord(@Nullable Boolean nullableProp, Boolean prop) {
}
// These are supposed to fail:
var serialized1 = "{\"nullableProp\": true, \"prop\": null}";
var serialized2 = "{\"nullableProp\": true}";
// These are not supposed to fail:
var serialized3 = "{\"nullableProp\": null, \"prop\": true}";
var serialized4 = "{\"prop\": true}";
var deserialized1 = objectMapper.readValue(serialized1, TestRecord.class);
var deserialized2 = objectMapper.readValue(serialized2, TestRecord.class);
var deserialized3 = objectMapper.readValue(serialized3, TestRecord.class);
var deserialized4 = objectMapper.readValue(serialized4, TestRecord.class);
I have tried 2 approaches for this, none of them worked:
1) Enable DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES
and DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES
on my objectMapper:
var objectMapper = new Jackson2ObjectMapperBuilder()
.featuresToEnable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES)
.featuresToEnable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
.build();
This will make Jackson throw for the cases where the @Nullable
property is null/absent, which I don't want. This made realize Jackson doesn't care about @Nullable
, so I tried the second approach which unfortunately also did not work:
2) Use a custom BeanDeserializerModifier
:
public class NonNullPropertyEnforcer extends BeanDeserializerModifier {
@Override
public JsonDeserializer<?> modifyDeserializer(
DeserializationConfig config,
BeanDescription beanDesc,
JsonDeserializer<?> deserializer
) {
if (deserializer instanceof BeanDeserializer beanDeserializer) {
Iterator<SettableBeanProperty> iterator = beanDeserializer.properties(); // no luck with creatorProperties() either
while (iterator.hasNext()) {
SettableBeanProperty property = iterator.next();
if (property.getAnnotation(Nullable.class) == null) {
SettableBeanProperty updatedProperty = property.withNullProvider(new NullValueProvider() {
@Override
public Object getNullValue(DeserializationContext ctxt) {
throw new IllegalArgumentException(
"Property '" + property.getName() + "' must not be null"
);
}
@Override
public AccessPattern getNullAccessPattern() {
return AccessPattern.DYNAMIC;
}
});
beanDeserializer.replaceProperty(property, updatedProperty);
}
}
}
return deserializer;
}
}
// No deserialization features enabled this time, just a simple module:
var nullableModule = new SimpleModule();
nullableModule.setDeserializerModifier(new NonNullPropertyEnforcer());
var objectMapper = new Jackson2ObjectMapperBuilder().modules(nullableModule).build();
This time, Jackson does not throw at all for any of the test cases. I debugged a little and found out that the code inside the methods of my NullValueProvider
isn't executed at all.
Thanks for any help with this!
Comment From: cowtowncoder
Quick note: we won't be adding direct dependency to Spring-specific annotations, so this would need to go in an add-on module.
Aside from that, I think it may make sense to instead look into how JacksonAnnotationIntrospector
detects annotations that relate to null handling: if one of callbacks would work -- possibly this one:
public JsonSetter.Value findSetterInfo(Annotated a);
it'd be supported throughout.
But one challenge is that @Nullable
is sort of inverse of required
flag so this might not be easy to make work overall.