icon-arrow icon-check icon-mail icon-phone icon-facebook icon-linkedin icon-youtube icon-twitter icon-cheveron icon-download icon-instagram play close close icon-arrow-uturn icon-calendar icon-clock icon-search icon-chevron-process icon-skills icon-knowledge icon-kite icon-education icon-languages icon-tools icon-experience icon-coffee-cup
Werken bij Integration & Application Talents
Blog 01/08/2023

Making Spring Boot applications return Bad Request on @JsonView ignored properties

Integration & Application Talents

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.

Whitehorses
Mike Heeren /
Integratie expert

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 jsonIgnoreFieldjsonIgnorePropertiesField 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 plaatsen

Reactie toevoegen

Jouw e-mailadres wordt niet openbaar gemaakt.

Geen HTML

  • Geen HTML toegestaan.
  • Regels en alinea's worden automatisch gesplitst.
  • Web- en e-mailadressen worden automatisch naar links omgezet.
Whitehorses
Mike Heeren /
Integratie expert

Wil je deel uitmaken van een groep gedreven en ambitieuze experts? Stuur ons jouw cv!