JAX-RS bardzo mocno usprawnia pracę z architekturą REST, pozwalając m.in. na wstrzykiwanie parametrów żądania do metody kontrolera (m.in. dzięki adnotacji @QueryParam). Pozwala to na automatyczną konwersję przysyłanych danych do większości używanych prostych typów. Co w przypadku, gdy zależy nam na konwersji nieobsługiwanego typu? Żaden problem, wystarczy napisać własny konwerter.

Aby zobrazować problem, stwórzmy prosty kontroler zwracający datę z dnia następującego po przekazanej jako parametr GET. Aby uprościć sobie zadanie, użyjmy typu java.time.LocalDate z Javy w wersji 8:

import java.time.LocalDate;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

@Path(value = "/")
public class RestController {
	@GET
	@Path(value = "tomorrow")
	@Produces(MediaType.APPLICATION_JSON)
	public LocalDate getTomorrow(@QueryParam(value = "today") LocalDate today) {
		return today.plusDays(1);
	}
}

Efektem uruchomienia aplikacji z tym kontrolerem będzie wyjątek:

RESTEASY003875: Unable to find a constructor that takes a String param or a valueOf() or fromString() method for javax.ws.rs.QueryParam("today") on public java.time.LocalDate pl.wercia.example.jaxrs.controller.RestController.getTomorrow(java.time.LocalDate) for basetype: java.time.LocalDate

W przypadku klasy pisanej przez siebie, wystarczyłoby dodać konstruktor z parametrem typu String lub jedną z metod statycznych valueOf(String value) albo fromString(String value). Jeśli używamy typów wbudowanych lub z zewnętrznych bibliotek, a chcielibyśmy uniknąć opakowywania w dodatkowy typ, wtedy na ratunek przychodzą konwertery parametrów.

Napisanie konwertera sprowadza się do implementacji interfejsu javax.ws.rs.ext.ParamConverter:

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import javax.ws.rs.ext.ParamConverter;

public class LocalDateParamConverter implements ParamConverter<LocalDate> {
	@Override
	public LocalDate fromString(String localDateString) {
		return LocalDate.parse(localDateString);
	}
	@Override
	public String toString(LocalDate localDate) {
		return DateTimeFormatter.ISO_DATE.format(localDate);
	}
}

Należy przy tym pamiętać, że samo napisanie konwertera, jeszcze nie rozwiązuje problemu - kontener nadal nie wie, że powinien go użyć w przypadku konkretnego typu. Aby wskazać to jawnie, wystarczy zaimplementować interfejs javax.ws.rs.ext.ParamConverterProvider zaadnotowany za pomocą javax.ws.rs.ext.Provider:

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.util.Objects;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;

@Provider
public class LocalDateParamConverterProvider implements ParamConverterProvider {
	@Override
	public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
		if (Objects.equals(rawType, LocalDate.class)) {
			return (ParamConverter<T>) new LocalDateParamConverter();
		}
		return null;
	}
}

Dzięki temu, po uruchomieniu aplikacji i wysłaniu żądania GET http://localhost:8080/example-jax-rs-query-converter/rest/tomorrow?today=2018-08-29 uzyskamy w odpowiedzi zserializowany obiekt java.time.LocalDate reprezentujący datę 30.08.2018:

{
	"year": 2018,
	"month": "AUGUST",
	"era": "CE",
	"chronology": {
		"calendarType": "iso8601",
		"id": "ISO"
	},
	"dayOfYear": 242,
	"dayOfWeek": "THURSDAY",
	"leapYear": false,
	"dayOfMonth": 30,
	"monthValue": 8
}

Przykładowy projekt: