8

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?

Rinke
  • 6,095
  • 4
  • 38
  • 55
  • This usually happens if the `JavaTimeModule` is not registered in the `ObjectMapper`: https://github.com/FasterXML/jackson-modules-java8#user-content-registering-modules –  Sep 22 '17 at 11:50
  • @Hugo I read about this, but it's unclear to me how to do this registration. Could you flesh this out into an answer? – Rinke Sep 22 '17 at 11:52
  • I've never used both jersey & jackson together, so not sure about how to do it. I usually set the `ObjectMapper` programatically (like in the link provided in previous comment). Anyway, maybe this can help: https://stackoverflow.com/q/18872931/7605325 –  Sep 22 '17 at 11:56
  • @Hugo I don't understand where to get "the" ObjectMapper, or where to put a newly created one. Simply putting a `new ObjectMapper().registerModule(new JavaTimeModule())` somewhere in my code doesn't magically make it work. This information is what's missing from everything I find on this topic. Can you explain this? – Rinke Sep 22 '17 at 12:35
  • As I said, I've never used jersey with jackson, so I really don't know where to configure it. Have you tried [this](https://stackoverflow.com/questions/20563640/using-jackson-objectmapper-with-jersey)? –  Sep 22 '17 at 12:42
  • @Hugo Actually I don't want to depend on Jersey, but just the JAX-RS API. Jersey is only included for the tests. I looked at all your examples, but there's a gap between the code snippets there, and my code. I don't understand how to transpose the offered solutions to my situation. Perhaps you understand this better and could do the translation? It would probably be a good answer. – Rinke Sep 22 '17 at 12:48
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/155140/discussion-between-rinke-and-hugo). – Rinke Sep 23 '17 at 09:54

2 Answers2

8

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 course 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);
    }
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Martijn
  • 111
  • 2
  • Instead of a `JacksonJaxbJsonProvider`, I'm using a `ResteasyJackson2Provider` and register with a priority as suggested by https://stackoverflow.com/a/47200638/698168 because the ResteasyJackson2Provider was deserializing the LocalDate before my custom mapper. But I'm using the same `ObjectMapper` configuration as you. – Julien Kronegg Jul 20 '20 at 08:14
3

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.

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720