Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson (de)serialization of Java8 date/time by a JAX-RS client

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?

like image 234
Rinke Avatar asked Sep 22 '17 10:09

Rinke


2 Answers

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.

like image 191
Martijn Avatar answered Oct 22 '22 05:10

Martijn


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.

like image 44
Paul Samsotha Avatar answered Oct 22 '22 07:10

Paul Samsotha