I'm making a serivce client for a REST endpoint, using a JAX-RS client for the HTTP requests and Jackson to (de)serialize JSON entities. In order to handle JSR-310 (Java8) date/time objects I added the com.fasterxml.jackson.datatype:jackson-datatype-jsr310
module as a dependency to the service client, but I didn't get it to work.
How to configure JAX-RS and/or Jackson to use the jsr310 module?
I use the following dependencies:
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>${jax-rs.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
I don't want to make the service client (which is released as a library) dependent on any specific implementation – like Jersey, so I only depend on the JAX-RS API. To run my integration tests I added:
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
Instantiation of the JAX-RS client is done in a factory object, as follows:
import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
@Produces
public Client produceClient() {
return ClientBuilder.newClient();
}
A typical DTO looks like this:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import java.time.Instant;
import static java.util.Objects.requireNonNull;
@JsonPropertyOrder({"n", "t"})
public final class Message {
private final String name;
private final Instant timestamp;
@JsonCreator
public Message(@JsonProperty("n") final String name,
@JsonProperty("t") final Instant timestamp) {
this.name = requireNonNull(name);
this.timestamp = requireNonNull(timestamp);
}
@JsonProperty("n")
public String getName() {
return this.name;
}
@JsonProperty("t")
public Instant getTimestamp() {
return this.timestamp;
}
// equals(Object), hashCode() and toString()
}
Requests are done like this:
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
public final class Gateway {
private final WebTarget endpoint;
public Message postSomething(final Something something) {
return this.endpoint
.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.json(something), Message.class);
}
// where Message is the class defined above and Something is a similar DTO
}
JSON serialization and deserialization works fine for Strings, ints, BigIntegers, Lists, etc. However, when I do something like System.out.println(gateway.postSomthing(new Something("x", "y"));
in my tests I get the following exception:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.Instant` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('Fri, 22 Sep 2017 10:26:52 GMT')
at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 562] (through reference chain: Message["t"])
at org.example.com.ServiceClientTest.test(ServiceClientTest.java:52)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('Fri, 22 Sep 2017 10:26:52 GMT')
at [Source: (org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream); line: 1, column: 562] (through reference chain: Message["t"])
at org.example.com.ServiceClientTest.test(ServiceClientTest.java:52)
From which I conclude that Jackson doesn't know how to deserialize Strings into Instants. I found blogs and SO questions about this topic, but I found no clear explanation on how to make it work.
Note that I'd like the service client to handle date strings like "Fri, 22 Sep 2017 10:26:52 GMT"
as well as "2017-09-22T10:26:52.123Z"
, but I want it to always serialize to ISO 8601 date strings.
Who can explain how to make deserialization into an Instant
work?
In the example code you're currently depending on jersey-media-json-jackson. You're probably better of by depending on Jackson's JAX-RS JSON as you are able to configure the Jackson mapper using the standard JAX-RS API (and of cource the Jackson API).
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>${jackson.version}</version>
</dependency>
After removing the jersey-media-json-jackson and adding the jackson-jaxrs-json-provider dependency you can configure the JacksonJaxbJsonProvider and register it in the class that produces the Client:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import static com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider.DEFAULT_ANNOTATIONS;
public class ClientProducer {
private JacksonJsonProvider jsonProvider;
public ClientProducer() {
// Create an ObjectMapper to be used for (de)serializing to/from JSON.
ObjectMapper objectMapper = new ObjectMapper();
// Register the JavaTimeModule for JSR-310 DateTime (de)serialization
objectMapper.registerModule(new JavaTimeModule());
// Configure the object mapper te serialize to timestamp strings.
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
// Create a Jackson Provider
this.jsonProvider = new JacksonJaxbJsonProvider(objectMapper, DEFAULT_ANNOTATIONS);
}
@Produces
public Client produceClient() {
return ClientBuilder.newClient()
// Register the jsonProvider
.register(this.jsonProvider);
}
}
Hope this helps.
You can configure the Jackson ObjectMapper
in a ContextResolver
. The Jackson JAX-RS provider will lookup this resolver and get the ObjectMapper
from it.
@Provider
public class ObjectMapperResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public ObjectMapperResolver() {
mapper = new ObjectMapper();
// configure mapper
}
@Override
public ObjectMapper getContext(Class<?> cls) {
return mapper;
}
}
Then just register the resolver like you would any other provider or resource in your JAX-RS application.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With