16

I'm trying to marshal response containing ISO formatted timestamp like that:

{
...
    "time" : "2014-07-02T04:00:00.000000Z"
...
}

into ZonedDateTime field in my domain model object. Eventually it works if I use solution that is commented in following snippet.There are many similar questions on SO but I would like to get a specific answer what is wrong with another approach which uses JacksonJsonProvider with ObjectMapper + JavaTimeModule?

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        JacksonJsonProvider provider = new JacksonJsonProvider(mapper);
        Client client = ClientBuilder.newBuilder()
//                .register(new ObjectMapperContextResolver(){
//                    @Override
//                    public ObjectMapper getContext(Class<?> type) {
//                        ObjectMapper mapper = new ObjectMapper();
//                        mapper.registerModule(new JavaTimeModule());
//                        return mapper;
//                    }
//                })
                .register(provider)
                .register(JacksonFeature.class)
                .build();

Error I get:

javax.ws.rs.client.ResponseProcessingException: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of java.time.ZonedDateTime: no String-argument constructor/factory method to deserialize from String value ('2017-02-24T20:46:05.000000Z')
 at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream@53941c2f

Project dependencies are:

compile 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.8.7'
compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.8.7'
compile 'com.fasterxml.jackson.core:jackson-core:2.8.7'
compile 'com.fasterxml.jackson.core:jackson-databind:2.8.7'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.8.7'
compile 'org.glassfish.jersey.core:jersey-client:2.25.1'

edit

Deserialization happens in here:

CandlesResponse<BidAskCandle> candlesResponse = webTarget.request()
                .header(HttpHeaders.AUTHORIZATION,"Bearer "+token)
                .accept(MediaType.APPLICATION_JSON)
                .get(new GenericType<CandlesResponse<BidAskCandle>>(){});
libnull-dev
  • 881
  • 1
  • 7
  • 19

3 Answers3

13

Eventually it works if I use solution that is commented in following snippet.

First of all, you are missing a dependency in your list, that you also have, which is the problem.

jersey-media-json-jackson

This module depends on the native Jackson module that has the JacksonJsonProvider. When you register the JacksonFeature (that comes with jersey-media-json-jackson), it registers its own JacksonJaxbJsonProvider, which seems to take precedence over any that you provide.

When you use the ContextResolver, the JacksonJsonProvider actually looks-up that ContextResolver and uses it to resolve the ObjectMapper. That's why it works. Whether you used the JacksonFeature or registered your own JacksonJsonProvider (without configuring an ObjectMapper for it) the ContextResovler would work.

Another thing about the jersey-media-json-jackson module, it that it participates in Jersey's auto-discoverable mechanism, which registers it's JacksonFeature. So even if you didn't explicitly register it, it would still be registered. The only ways to avoid it being registered are to:

  1. Disable the auto-discovery (as mention in the previous link)1

  2. Don't use the jersey-media-json-jackson. Just use the Jackson native module jackson-jaxrs-json-provider. Thing about this though is that, the jersey-media-json-jackson adds a couple features on top of the the native module, so you would lose those.

  3. Haven't tested, but it seems that if you use JacksonJaxbJsonProvider instead of JacksonJsonProvider, it might work. If you look at the source for the JacksonFeature, you will see that it checks for an already registered JacksonJaxbJsonProvider. If there is one, it won't register it's own.

    The one thing I'm not sure about with this is the auto-discoverable. The order in which it is registered, if it will affect whether or not it catches your registered JacksonJaxbJsonProvider. Something you can test out.


Footnotes

1. See also

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • 2
    I chose option with `JacksonJaxbJsonProvider` as it looks more straightforward and doesn't involve extra dependencies. It works. – libnull-dev Feb 28 '17 at 21:03
  • the previous link turns out to be a hanging reference by now – inxoy Jul 09 '19 at 06:21
  • @inxoy I just fixed it. Thanks for the heads up. – Paul Samsotha Jul 09 '19 at 06:25
  • The "disable the auto-discovery" link is dead now since Jersey changed their pages, suggest you link to this question instead: https://stackoverflow.com/questions/30278303/jersey-disable-default-json-provider – Mr Chow Sep 25 '21 at 19:00
8

From my pet project:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>${jackson.version}</version>
</dependency>

public WebTarget getTarget(URI uri) {
    Client client = ClientBuilder
            .newClient()
            .register(JacksonConfig.class);
    return client.target(uri);
}

where

@Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {

    private final ObjectMapper objectMapper;

    public JacksonConfig() {
        objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    @Override
    public ObjectMapper getContext(Class<?> aClass) {
        return objectMapper;
    }
}
Mircea Stanciu
  • 3,675
  • 3
  • 34
  • 37
3

Jackson configuration looks fine, I tried the following and was able to deserialize the value:

public class Test {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        Model model = mapper.readValue("{\"time\" : \"2014-07-02T04:00:00.000000Z\"}", Model.class);
        System.out.println(model.getTime());
    }

}

class Model{
    private ZonedDateTime time;

    public ZonedDateTime getTime() {
        return time;
    }
    public void setTime(ZonedDateTime time) {
        this.time = time;
    }
}

I can reproduce it by commenting out mapper.registerModule(new JavaTimeModule());. So, it looks like jersey client is not using custom mapper instance. Could you try configuring it as described here.

Darshan Mehta
  • 30,102
  • 11
  • 68
  • 102
  • Tried, no change :( – libnull-dev Feb 26 '17 at 12:08
  • Do you mean this? `ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); JacksonJsonProvider provider = new JacksonJsonProvider(); provider.setMapper(mapper); ClientConfig config = new ClientConfig(provider); Client client = ClientBuilder.newClient(config);` However this doesn't work either. – libnull-dev Feb 26 '17 at 13:39
  • Could you post the code that performs the deserialization? – Darshan Mehta Feb 26 '17 at 13:47