Making Spring Boot applications return Bad Request on @JsonView ignored properties
For a project I’m currently working on, we’re using Spring Boot for developing REST services. For fields that can only be provided in certain requests, we’re using the Jackson @JsonView annotation. For example, we have some records where the identifier value can only be specified in the insert (POST) operation, but those values can’t be updated later by using the PUT operation. Besides just ignoring these values, we also wanted to provide feedback to the consuming application by returning a “400 Bad Request” response. This way the consumer can’t make the (false) assumption that our service actually does something with these ignored values.
Example application
While developing this feature I created an example application. This application exposes a REST service with a POST and PUT operation, both accepting the following record:
@JsonIgnoreProperties("jsonIgnorePropertiesField")
public record Example(
@JsonIgnore String jsonIgnoreField,
String jsonIgnorePropertiesField,
@JsonView(Views.PostRequestView.class) String postOnlyField,
@JsonView(Views.PutRequestView.class) String putOnlyField,
@JsonView(Views.ResponseView.class) String responseOnlyField) {
}
Basically, what we want to achieve is that the POST-requests only accept the following field:
{
"postOnlyField": "Request value"
}
The PUT-requests on the other hand, should only accept the following field:
{
"putOnlyField": "Request value"
}
When any other field (that’s either not specified in the record at all, or ignored by any of the Jackson annotations), we want to return a 400 Bad Request. Unfortunately, the default behavior from Jackson is that these values will just not be included while unmarshalling/deserializing the JSON structure to the Java object (and thus resulting in a 200 OK response).
Jackson properties
Luckily, Spring Boot provides the possibility to instruct the Jackson ObjectMapper to throw an exception when ignored or unknown properties are found while deserializing. We can do this by specifying the following application properties:
spring:
jackson:
deserialization:
fail-on-ignored-properties: true
fail-on-unknown-properties: true
This works fine for the jsonIgnoreField, jsonIgnorePropertiesField and any undefined property. However, the properties that are ignored by the @JsonView annotation, are still not resulting in an error.
Making Jackson fail on @JsonView
On the FasterXML jackson-databind project we can also find the following issue regarding this behaviour: #437 - UnrecognizedPropertyException is not thrown when deserializing properties that are not part of the view. This issue is still open and labeled for the 3.x version of the library. So hopefully in the future, this issue can just be resolved by specifying another property. But since there’s currently no support for this feature, we have to go for a workaround.
The previously mentioned issue also links to the following (closed) issue: #1956 - BeanDeserializerModifier not called again for changed config. The attempt here was a workaround by using a custom BeanDeserializerModifier. But as mentioned in the comments, this won’t work when a single ObjectMapper instance needs to be used for different views (which is what Spring does by default), since the deserializers are created only once and cached.
However, these comments also mention the ObjectReader instances that can be created from the ObjectMapper. When having a look at the MappingJackson2HttpMessageConverter (which Spring uses by default for (de)serializing objects from/to JSON structures), we can see that it provides a possibility to customize these ObjectReader instances by overriding the customizeReader method. Within this method we have access to both the active view and the default ObjectMapper implementation.
This custom implementation checks if a @JsonView is active. If that’s the case, we will generate a view-specific ObjectMapper, cache it and return a new ObjectReader generated from this new ObjectMapper instead of the default one:
@Component
public class ActiveViewAwareMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
private final Map<Class<?>, ObjectMapper> activeViewAwareObjectMappers = new HashMap<>();
public ActiveViewAwareMappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper);
}
@Override
protected ObjectReader customizeReader(ObjectReader reader, JavaType javaType) {
var activeView = reader.getConfig().getActiveView();
if (activeView != null) {
var customViewObjectMapper = activeViewAwareObjectMappers.computeIfAbsent(activeView, view -> {
var module = new SimpleModule();
module.setDeserializerModifier(new IgnoreJsonViewBeanDeserializerModifier(view));
return getObjectMapper().copy().registerModule(module);
});
return customViewObjectMapper.readerWithView(activeView).forType(javaType);
}
return super.customizeReader(reader, javaType);
}
@RequiredArgsConstructor
private static class IgnoreJsonViewBeanDeserializerModifier extends BeanDeserializerModifier {
private final Class<?> view;
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config,
BeanDescription beanDesc,
BeanDeserializerBuilder builder) {
var propertyIterator = builder.getProperties();
while (propertyIterator.hasNext()) {
final var property = propertyIterator.next();
if (!property.visibleInView(view)) {
builder.addIgnorable(property.getName());
}
}
return builder;
}
}
}
Conclusion
With a bit of custom code we achieved our goal of returning a 400 Bad Request response for all ignored- and unknown properties in JSON requests.
Still, it would be really nice if the 3.x release of the FasterXML jackson-databind library would contain a solution for this issue.
All code from the example(s) can be found on the Whitehorses Bitbucket page:
https://bitbucket.org/whitehorsesbv/spring-ignored-properties-json-view
Geen reacties
Geef jouw mening
Reactie plaatsenReactie toevoegen