Read a query parameter as Json with JAX-RS
JAX-RS enables to map parameters through converters. It is more and more common to have a query parameter being some (URI encoded) JSON. A common example is to have an evolutive querying solution - on top of JPA is it very efficient to give a concrete example.
To illustrate this post, we will take the pagination example where we want to pass the pagination request through a parameter named p.
Here is a request example:
// {"page": 2, "size": 2}
http://localhost:8080/api/entities?p=%7B%22page%22%3A2,%22size%22%3A2%7D
Of course, you can implement it in a specific way each time you need:
@GET
public Page getPage(@QueryParam("p")
@DefaultValue(PageRequest.DEFAULT)
final String pageRequest) {
final PageRequest pageRequest;
try (final StringReader reader = new StringReader(pageRequest)) {
pageRequest = jsonb.fromJson(reader, PageRequest.class);
}
return doQuery(MyEntity.class, pageRequest);
}
However, this is hard to generalize with other parameters - like the filter one ;) - and will need to be done in all methods which is not that nice to maintain.
To solve that issue, we can use JAX-RS converters.
To identify, in a generic fashion, the types which must use a custom JSON converter, we will create an annotation we will put on the related types:
@Target(TYPE)
@Retention(RUNTIME)
public @interface Jsonified {
}
We, then modify our PageRequest to look like this:
@Data // getters/setters
@Jsonified
public class PageRequest {
private int page;
private int size;
}
Then, the first thing to do to let JAX-RS know how to convert our request is to implement a ParamConverterProvider which will get injected a Jsonb instance - you can use any other Json deserializer you want depending the types you want to map, Jsonb has the advantage to handle POJO and JSON-P types:
@Provider
@Dependent
public class JsonifiedParamConverterProvider implements ParamConverterProvider {
@Inject
private Jsonb jsonb;
@Override
public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType, final Annotation[] annotations) {
if (rawType.isAnnotationPresent(Jsonified.class)) {
return new JsonifiedConverter<>(jsonb, rawType);
}
return null;
}
}
Finally, we need to implement this JsonifiedConverter converter to actually do the deserialization:
@RequiredArgsConstructor
private static class JsonifiedConverter<T> implements ParamConverter<T> {
private final Jsonb jsonb;
private final Class<T> type;
@Override
public T fromString(final String value) {
try (final StringReader reader = new StringReader(value)) {
return jsonb.fromJson(reader, type);
}
}
@Override
public String toString(final T value) {
throw new UnsupportedOperationException();
}
}
Now, we can simply inject our parameter in our endpoint without caring of the incoming format:
@GET
public Page getPage(@QueryParam("p")
@DefaultValue(PageRequest.DEFAULT)
final PageRequest pageRequest) {
return doQuery(MyEntity.class, pageRequest);
}
The nice thing is that you can compose more easily libraries and incoming parameters since they host their contract - or you can override it with a custom converter for external types. This way the business code becomes really easy and the reuse of your code is enabled, increasing again your productivity.
From the same author:
In the same category: