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: