We have a rest api built with spring boot and the openapi code generator. Let's assume there is a path /user
in our api spec:
...
"paths": {
"/user": {
"get": {
"parameters": [{
"name": "id",
"in": "query",
"required": false,
"type": "integer",
"format": "int64"
}],
"responses": { ... }
},
}
// more paths
}
...
A call to this path could then be: /user?id=1234
.
The code generator creates an interface MyControllerApi:
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "some date")
@Api(value="user")
public interface MyControllerApi {
@ApiOperation(value="", nickname="userGet", response = User.class, /* ... */)
@ApiResponse(/* ... */)
@GetMapping(value="/user", produces = { "application/json" })
ResponseEntity<User> userGet(@ApiParam(value = "id") @RequestParam(value = "id", required = false) Long id);
}
The controller then looks like this:
@RestController
public class MyController implements MyControllerApi
{
@Autowired
UserService service;
@Override
public ResponseEntity<User> userGet(@RequestParam(value = "id") Long id) {
return service.get(id);
}
}
If /user?id=<value>
is requested, spring boot automatically checks if the type of the passed parameter value <value>
matches the required type. If not BAD_REQUEST
is returned:
{
"timestamp": "2022-10-19T17:20:48.393+0000",
"status": 400,
"error": "Bad Request",
"path": "/user"
}
We are now in a situation in which we want to pass null
to each parameter of userGet
that would cause a type mismatch. To be more clear: if /user?id=abcd
is requested, userGet
should be called with id
set to null, so that we can return some default user. Is there a way to achieve this?
Of course there are a lot more paths and parameters and this behavior should apply for every query parameter of type Long
or Boolean
.
This example may not make much sense, but it is also just an example.
Thanks in advance
useOptional
option ...... to true
in the pom.xml (see here).
This has the affect that the query parameters in the controllers method are of type Optional<?>
. In my example above this would lead to:
@Override
public ResponseEntity<User> userGet(Optional<Long> id) {
Long id_val = id.isPresent() ? id.get() : null;
return service.get(id_val);
}
But this did not work either and spring boot also creates a BAD_REQUEST
response on parameter type mismatches.
A request interceptor is a kind of middleware and can be created by implementing HandlerInterceptor
and enables you to process the request before it is passed to the controller.
My interceptor looks like this:
public class CustomRequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
HandlerMethod method = (HandlerMethod) handler;
MethodParameter[] handlerParams = method.getMethodParameters();
while (request.getParameterNames().hasMoreElements())
{
// the param name of the query
String paramName = request.getParameterNames().nextElement();
the param value
String paramValue = request.getParameter(paramName);
MethodParameter methodParam = null;
for (MethodParameter mp : handlerParams)
{
// We can access annotations here ...
Annotation anno = mp.getParameterAnnotation(RequestParam.class);
if(anno != null)
{
RequestParam rp = (RequestParam) anno;
// ... and am able to get the param name in the controller
// Check if we found the param
String requestParamName = rp.value();
if (requestParamName.toLowerCase().equals(paramName.toLowerCase()))
{
// and now?
break;
}
}
}
if (methodParam != null)
{
Type type = methodParam.getGenericParameterType();
}
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
So far so good, but the RequestParam-Object does not hold any informations about the parameter type, nor about the index of that param in the methods param list. And the MethodParameter does not hold the name of the parameter (because its a compiled class I think).
What I'm wondering is how spring boot itself maps the query parameters to the ones in the controllers methods..... ?
After doing some research I found a solution by using a custom HandlerMethodArgumentResolver
:
public class MyCustomResolver implements HandlerMethodArgumentResolver {
@Override
@Nullable
public Object resolveArgument(
MethodParameter methodParam,
@Nullable ModelAndViewContainer mvContainer,
NativeWebRequest request,
@Nullable WebDataBinderFactory binderFac
) throws Exception {
String paramValue = request.getParameter(methodParam.getParameterName());
if (paramValue == null)
return null;
if (methodParam.getParameterType().equals(Long.class)) {
try {
// Set param value to the parsed one, if it can be parsed
return Long.parseLong(paramValue);
} catch (Exception e) {
// Set param value to null otherwise
return null;
}
}
return null;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Long.class));
}
}
The method supportsParameter()
gives you the opportunity to decide whether or not the passed parameter will be processed in resolveArgument
.
resolveArgument
then gets the parameter as MethodParameter
. Unlike in the HandlerInterceptor
the MethodParameter
now contains the parameter name and type.
Using this approach id
will have the following values:
id = 1 on /user?id=1
id = null on /user?id=abc
Now I'am able to handle falsy parameter values in the controller directly.
If you use a parameter of type Boolean
the param value will not be null but "false" for every parameter value except true
. Only if "true" is passed in the request resolveArgument
will parse it to true
and getUser
will receive true
.