A while ago I was working on a Spring REST project and had a special wish. I wanted for one endpoint an exception thrown when someone requested it with an object with unknown properties. All the other endpoints with their rest objects should continue to exhibit the same behaviour. Let me share how I did this.

Ignorance is our deepest secret

Although I want to dive right in the problem, first I need to make sure you understand the default behaviour of a Spring Rest Controller. Imagine you got a JSON structure that is represented by following POJO:

{
  "name": "Hank",
  "birthDate": "1998-09-14"
}
@Data
public class User {
  private String name;
  private LocalDate birthDate;
}

And you want to send this data to a server to report the age of this user back to you. We can easily do this by creating a @RestController class with a @PostMapping annotated method. Then map the incoming data to our user object and add the @RequestBody annotation before the user parameter:

@RestController
public class UserController {
  @PostMapping("/users/description")
  public String getDescription(@RequestBody User user) {
    final var age = Period.between(user.getBirthDate(), LocalDate.now()).getYears();
    return user.getName() + " is " + age + " years old";
  }
}

The joy is in remembering

You’ve probably done this a thousand times. But are you aware what happens behind the scenes when you use the @RequestBody annotation? It tells Spring to bound your annotated parameter to the body of a web request. The body is expected to be of JSON format. Once a request comes in, it is passed to a HttpMessageConverter class that translates the JSON content to a POJO.

Under the hood Spring itself uses the Jackson library to achieve this. This library provides an object called ObjectMapper to configure the actual deserialization. By default, Spring will create one ObjectMapper bean for you; this bean is setupped with some opinionated defaults. One of them is the disabling of the FAIL_ON_UNKNOWN_PROPERTIES flag. This means that your JSON may consist of fields that do not exist in your POJO.

Most of the time this default is fine, or even preferable. For my case I wanted most objects to follow this strategy as well. But then, for some objects I actually liked to see a failure message when the JSON consisted of unknown fields. So I figured, it would be nice if I could use the existing @RequestBody annotation to behave like the standard, while a @StrictRequestBody annotation would use a self defined ObjectMapper bean where the unknown properties flag is set to true.

That question is too good to spoil with an answer

But how could we do this? First let’s create the annotation. This annotation should be identical to the original:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface StrictRequestBody {
  boolean required() default true;
}

To let it work, we need to define our own mapper of course. We can create a new class that extends the default ObjectMapper. Then it’s just as easy as registering the default modules and configuring the flags you want. In our case, we shouldn’t even have to configure anything, because the fail on unknown properties flag is true by default. But just to give you an insight how you can construct such a mapper, I’ll add the configuration anyway.

public class OnlyKnownPropertiesObjectMapper extends ObjectMapper {
  public OnlyKnownPropertiesObjectMapper() {
    findAndRegisterModules();
    configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
  }
}

Resolve to do each job in a relaxed way

If we want to let the @StrictRequestBody annotated web requests do anything, we need some kind of processor that picks up requests and translates JSON to POJOs. Just for that goal Spring provides the HandlerMethodArgumentResolver interface. Before we dive into the implementation, let’s first create our own resolver class:

public class StrictRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
  @Override
  public boolean supportsParameter(MethodParameter mp) {}

  @Override
  public Object resolveArgument(MethodParameter mp, ModelAndViewContainer m, NativeWebRequest request, WebDataBinderFactory f) throws Exception {}
}

As you can see, the interface prescribes the supportsParameter and the resolveArgument methods. With the supports parameter method you can filter whether the incoming request should be handled. In our case we only want to handle requests that have our annotation:

public boolean supportsParameter(MethodParameter mp) {
  return mp.getParameterAnnotation(StrictRequestBody.class) != null;
}

As you probably will have guessed by now, the argument resolver method does the actual conversion. By retrieving the request as inputstream, we can use our own mapper to translate the object[1]:

public Object resolveArgument(MethodParameter mp, ModelAndViewContainer m, NativeWebRequest request, WebDataBinderFactory f) throws Exception {
  final var inputStream = ((ServletRequest) request.getNativeRequest()).getInputStream();
  final var objectMapper = new OnlyKnownPropertiesObjectMapper();

  return objectMapper.readValue(inputStream, mp.getParameterType());
}

Something to note here, if the mapper cannot deserialize the content into the given Java type, it will throw either a IOException, JsonParseException or JsonMappingException. You shouldn’t catch these exceptions, because Spring provides several general ways to do REST error handling; click here for a nice overview.

We stand at a crossroad

Now, that’s starting to look like it. There is only one thing left to do. Spring needs to know about our resolver:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new StrictRequestBodyArgumentResolver());
  }
}

That’s your solution? Have a cookie!

And that’s it! All the files are in place, now let’s use it:

@PostMapping("/users/description")
public String getDescription(@StrictRequestBody User user) {
  final var age = Period.between(user.getBirthDate(), LocalDate.now()).getYears();
  return user.getName() + " is " + age + " years old";
}

This code in this blog is based upon this Stack Overflow answer. Though all content on Stackoverflow is licensed under CC license, I contacted Chimmi directly to get his permission.


1. For a production ready solution, you also need to handle requests with an empty body. The entire class can be found here.
shadow-left