Required @QueryParam in JAX-RS (and what to do in their absence)
Good question. Unfortunately (or maybe fortunately) there is no mechanism in JAX-RS to make any params mandatory. If a parameter is not supplied it's value will be NULL
and your resource should deal with it accordingly. I would recommend to use WebApplicationException
to inform your users:
@GET
@Path("/some-path")
public String read(@QueryParam("name") String name) {
if (name == null) {
throw new WebApplicationException(
Response.status(Response.Status.BAD_REQUEST)
.entity("name parameter is mandatory")
.build()
);
}
// continue with a normal flow
}
You can use javax.validation
annotations to enforce that the parameters are mandatory by annotating them with @javax.validation.constraints.NotNull
. See an example for Jersey and one for RESTeasy.
So your method would simply become:
@GET
@Path("/some-path")
public String read(@NotNull @QueryParam("name") String name) {
String something =
// implementation
return something;
}
Note that the exception gets then translated by the JAX-RS provider to some error code. It can usually be overridden by registering your own implementation of javax.ws.rs.ext.ExceptionMapper<javax.validation.ValidationException>
.
This provides a centralized way to translate mandatory parameter to error responses and no code duplication is necessary.
I ran into the same problem and decided that I did not want a gazillion boilerplate null checks scattered across my REST code, so this this is what I decided to do:
- Create an annotation that causes an exception to be thrown when a required parameter is not specified.
- Handle the thrown exception the same way I handle all other exceptions thrown in my REST code.
For 1), i implemented the following annotation:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Required
{
// This is just a marker annotation, so nothing in here.
}
... and the following JAX-RS ContainerRequestFilter
to enforce it:
import java.lang.reflect.Parameter;
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
@Provider
public class RequiredParameterFilter implements ContainerRequestFilter
{
@Context
private ResourceInfo resourceInfo;
@Override
public void filter(ContainerRequestContext requestContext)
{
// Loop through each parameter
for (Parameter parameter : resourceInfo.getResourceMethod().getParameters())
{
// Check is this parameter is a query parameter
QueryParam queryAnnotation = parameter.getAnnotation(QueryParam.class);
// ... and whether it is a required one
if (queryAnnotation != null && parameter.isAnnotationPresent(Required.class))
{
// ... and whether it was not specified
if (!requestContext.getUriInfo().getQueryParameters().containsKey(queryAnnotation.value()))
{
// We pass the query variable name to the constructor so that the exception can generate a meaningful error message
throw new YourCustomRuntimeException(queryAnnotation.value());
}
}
}
}
}
You need to register the ContainerRequestFilter
in the same way you would register your other @Provider
classes with your JAX-RS library. Maybe RESTEasy does it for you automatically.
For 2), I handle all runtime exceptions using a generic JAX-RS ExceptionMapper
:
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class MyExceptionMapper implements ExceptionMapper<RuntimeException>
{
@Override
public Response toResponse(RuntimeException ex)
{
// In this example, we just return the .toString() of the exception.
// You might want to wrap this in a JSON structure if this is a JSON API, for example.
return Response
.status(Response.Status.BAD_REQUEST)
.entity(ex.toString())
.build();
}
}
As before, remember to register the class with your JAX-RS library.
Probably the easiest way is to use @Nonnull
from javax.annotation
to achieve this. It's super simple to use as all you have to do is add it before @QueryParam
as shown below.
However, keep in mind that this will throw an IllegalArgumentException
when the parameter is null so the response you send back will be whatever you do for an exception. If you don't intercept it it's going to be a 500 Server Error
even though the correct thing to send back would be a 400 Bad Request
. You can intercept IllegalArgumentException
and process it to return a proper response.
Example:
import javax.annotation.Nonnull;
...
@GET
@Path("/your-path")
public Response get(@Nonnull @QueryParam("paramName") String paramName) {
...
}
The default error message returned to the caller looks like this:
{"timestamp":1536152114437,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"Argument for @Nonnull parameter 'paramName' of com/example/YourClass.get must not be null","path":"/path/to/your-path"}