Unable to deserialize ZonedDateTime using Jackson - java

I am using Jersey with Jackson as JSON provider. I am able to serialize ZonedDateTime to JSON but when I want to deserialize it gives me error as follows.
Could you please help me tell the exact configuration required to get this deserialization work.
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class java.time.ZonedDateTime] from String value ('2016-01-21T21:00:00Z'); no single-String constructor/factory method
My mapper configuration is as follows:
#Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper MAPPER;
public ObjectMapperContextResolver() {
MAPPER = new ObjectMapper();
//This would add JSR310 (Datetime) support while converting date to JSON using JAXRS service
MAPPER.registerModule(new JavaTimeModule());
//Below line would disable use of timestamps (numbers),
//and instead use a [ISO-8601 ]-compliant notation, which gets output as something like: "1970-01-01T00:00:00.000+0000".
MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
}
#Override
public ObjectMapper getContext(Class<?> type) {
return MAPPER;
}
}

I found the problem, actually issue was not with Deserialization using standard jackson provider. In my case, I was using Jersey client to get JSON and then deserialize using readEntity method.
Problem was that jersey client was not aware of the jsr310 module, so by registering the contextresolver where jsr310 has been added solved the issue. So in nutshell, you don't need to do anything for seralization and deserialization of ZonedDateTime if using normal jackson provider.
Below is the reference code which I am referring here, to get better clarity.
public class RESTClientImpl{
/*
* ***This is very important, JacksonJsonProvider is the implementation of
* MessageBodyWriter/Reader which is required for "readEntity" method,
* else it would throw MessageBodyWriter/Reader not found exception
*
* https://jersey.java.net/documentation/latest/message-body-workers.html#mbw.ex.client.mbr.reg
*
* Registering of ObjectMapperContextResolver is important as we have registered JSR310 module there and without registering this,
* Jersey client is not aware of JSR310 module, so it will not be able to de-serialize ZonedDateTime
*/
private final Client client = ClientBuilder.newClient(new ClientConfig().register(LoggingFilter.class)).register(JacksonJsonProvider.class)
.register(ObjectMapperContextResolver.class);
public User get(URI uri) {
WebTarget webTarget = client.target(uri);
Invocation.Builder invocationBuilder = webTarget.request(MediaType.APPLICATION_JSON);
Response response = invocationBuilder.get();
User user = response.readEntity(User.class);
return user;
}
}
#Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper MAPPER;
public ObjectMapperContextResolver() {
MAPPER = new ObjectMapper();
//This would add JSR310 (Datetime) support while converting date to JSON using JAXRS service
MAPPER.registerModule(new JavaTimeModule());
//Below line would disable use of timestamps (numbers),
//and instead use a [ISO-8601 ]-compliant notation, which gets output as something like: "1970-01-01T00:00:00.000+0000".
MAPPER.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(Object.class, new ZonedDateTimeDeserializer());
MAPPER.registerModule(simpleModule);
}
#Override
public ObjectMapper getContext(Class<?> type) {
return MAPPER;
}
}

Related

Jackson ObjectMapper.findAndRegisterModules() not working to serialise LocalDateTime

I am using Java Spring-boot RestController. I have a sample GET API in which I am sending LocalDateTime.now() in response body. I have customised the Jackson ObjectMapper to register jackson-datatype-jsr310 module, However it fails to serialise the LocalDateTime instance. I have tried many different solutions available online, however nothing seems to work. The solution that I had before posting here is mentioned below.
The GET API gives the following error:
"Java 8 date/time type java.time.LocalDateTime not supported by
default: add Module
"com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable
handling (through reference chain:
org.springframework.http.ResponseEntity["body"])"
Code:
ObjectMapper Configuration:
#Configuration
public class JacksonConfiguration {
#Bean
#Primary
public ObjectMapper objectMapper2(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.build();
objectMapper.findAndRegisterModules();
return objectMapper;
}
}
Note: I have tried using objectMapper.registerModule(new JSR310Module()) and objectMapper.registerModule(new JavaTimeModule()). They don't work either.
Rest Controller:
#RestController
public class TestController {
#GetMapping("/test")
public ResponseEntity<Object> test() {
return ResponseEntity.ok().body(LocalDateTime.now());
}
}
I am using spring-boot-starter-parent 2.5.4, so it automatically uses version 2.12.4 for all jackson.* dependencies including jackson-datatype-jsr310.
Welcome to Stack Overflow, like documented at datetime the lines ObjectMapper objectMapper = builder.build();objectMapper.findAndRegisterModules(); you are currently using are valid just for jackson 2.x before 2.9 while your project includes the jackson 2.12.4 library : like the official documentation I linked before you have to use instead the following code :
// Jackson 2.10 and later
ObjectMapper mapper = JsonMapper.builder()
.findAndAddModules()
.build();
Or as an alternative if you prefer to selectively register the JavaTimeModule module :
// Jackson 2.10 and later:
ObjectMapper mapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build();
Update : I modified the original code proposed in the question to the following :
#Configuration
public class JacksonConfiguration {
#Bean
#Primary
public ObjectMapper objectMapper2(Jackson2ObjectMapperBuilder builder) {
ObjectMapper mapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build();
return mapper;
}
}
The javatime module works fine and returns the correct LocalTime json representation; without the configuration class the returned value is the correct iso string.

Swap Jackson custom serializer / deserializer during runtime

I have the following system:
I am sending MediaType.APPLICATION_JSON_VALUEs from spring controllers to my client and vice versa.
I also have an export/import feature of my to-be-serialized classes. The JSON File is created by using an ObjectMapper and utilizing the writeValueAsString and readValue methods. I am reading from and writing into the json file.
Both of those serialization paths currently utilize the same serializers/deserializers.
I use the #JsonSerialize and #JsonDeserialize annotations to define custom serialization for some of my objects.
I want to serialize those objects differently for export/import.
So I want to swap the serializer / deserializer for the export/import task. Something like this:
If I understand the docs correctly, those two annotations only allow one using class. But I want to register multiple serializers/deserializers and use them based on some conditional logic.
You might want to have two separate ObjectMapper instances configured for Server and Client.
Server module:
ObjectMapper serverMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ServerDTO.class, new CustomerFileSerializer());
module.addDeserializer(ServerDTO.class, new CustomerFileDeserializer());
serverMapper.registerModule(module);
ServerDTO serverDto = serverMapper.readValue(jsonInput, ServerDTO.class);
String serialized = serverMapper.writeValueAsString(serverDto);
and
Client module:
ObjectMapper clientMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(ClientDTO.class, new CustomerClientSerializer());
module.addDeserializer(ClientDTO.class, new CustomerClientDeserializer());
clientMapper.registerModule(module);
ClientDTO clientDTO = clientMapper.readValue(jsonInput, ClientDTO.class);
String serialized = clientMapper.writeValueAsString(clientDTO);
So I was trying to figure this out for the last few days. This is the progress I made so far:
I did two overrides for the default ObjectMapper in Spring and made sure they are configured like the default.
My custom mappers look like this:
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper defaultV7ObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerClientSerializer());
module.addDeserializer(Customer.class, new CustomerClientDeserializer());
objectMapper.registerModule(module);
return objectMapper;
}
#Bean("exportImportMapper")
public ObjectMapper exportImportMapper() {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
objectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerFileSerializer());
module.addDeserializer(Customer.class, new CustomerFileDeserializer());
objectMapper.registerModule(module);
return objectMapper;
}
}
I also removed the #JsonSerialize and #JsonDeserialize annotations from my entities.
HOWEVER there is one big difference with this change from annotations to adding the serializers via the module.
Let's say I have a class A that has a Customer property with the #JsonSerialize and #JsonDeserialize annotation.
Let's also say I have a class B that has a Customer property without annotations.
By removing the annotations and setting the serializer/deserializer as shown above I have now added theses serializers/deserializers to both Customer properties. So it's not equivalent.
Or am I missing something here?
This is my solution
It's not pretty but does its job.
I left my old jackson config untouched, so the client<->server serialization stays the same.
I then added this custom ObjectMapper to take care of my server<->file.
My custom ObjectMapper does the following things:
It registers a new custom JacksonAnnotationIntrospector, which I configured to ignore certain annotations. I also configured it to use my selfmade annotation #TransferJsonTypeInfo whenever a property has both the #TransferJsonTypeInfo as well as the #JsonTypeInfo annotation.
I registered my CustomerFileSerializer and CustomerFileDeserializer for this ObjectMapper.
#Service
public class ImportExportMapper {
protected final ObjectMapper customObjectMapper;
private static final JacksonAnnotationIntrospector IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO = BuildImportExportJacksonAnnotationIntrospector();
public ImportExportMapper(){
customObjectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
customObjectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
customObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerFileSerializer());
module.addDeserializer(Customer.class, new CustomerFileDeserializer());
customObjectMapper.setAnnotationIntrospector(IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO);
customObjectMapper.registerModule(module);
}
public String writeValueAsString(Object data) {
try {
return customObjectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new IllegalArgumentException();
}
}
public ObjectTransferData readValue(String fileContent, Class clazz) throws JsonProcessingException {
return customObjectMapper.readValue(fileContent, clazz);
}
private static JacksonAnnotationIntrospector BuildImportExportJacksonAnnotationIntrospector() {
return new JacksonAnnotationIntrospector() {
#Override
protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> annoClass) {
if (annoClass == JsonTypeInfo.class && _hasAnnotation(annotated, FileJsonTypeInfo.class)) {
FileJsonTypeInfo fileJsonTypeInfo = _findAnnotation(annotated, TransferJsonTypeInfo.class);
if(fileJsonTypeInfo != null && fileJsonTypeInfo.jsonTypeInfo() != null) {
return (A) fileJsonTypeInfo.jsonTypeInfo(); // this cast should be safe because we have checked the annotation class
}
}
if (ignoreJsonAnnotations(annoClass)) return null;
return super._findAnnotation(annotated, annoClass);
}
};
}
private static <A extends Annotation> boolean ignoreJsonAnnotations(Class<A> annoClass) {
if (annoClass == JsonSerialize.class) {
return true;
}
if(annoClass == JsonDeserialize.class){
return true;
}
if(annoClass == JsonIdentityReference.class){
return true;
}
return annoClass == JsonIdentityInfo.class;
}
}
My custom annotation is defined and described like this:
/**
* This annotation inside of a annotation solution is a way to tell the importExportMapper how to serialize/deserialize
* objects that already have a wrongly defined #JsonTypeInfo annotation (wrongly defined for the importExportMapper).
*
* Idea is taken from here: https://stackoverflow.com/questions/58495480/how-to-properly-override-jacksonannotationintrospector-findannotation-to-replac
*/
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface FileJsonTypeInfo {
JsonTypeInfo jsonTypeInfo();
}
And it is used like this:
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonTypeInfo(defaultImpl = Customer.class, property = "", use = JsonTypeInfo.Id.NONE)
#TransferJsonTypeInfo(jsonTypeInfo = #JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "customeridentifier"))
#JsonIdentityReference(alwaysAsId = true)
#JsonDeserialize(using = CustomerClientDeserializer.class)
#JsonSerialize(using = CustomerClientSerializer.class)
private Customer customer;

How to make injected objectmapper output locale-specific

I have a Spring Boot REST controller with the method
#GetMapping(value = "/validate", produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
#ResponseBody
public Result validate(
#ApiParam(value = "http://example.org/test", required = true) #RequestParam String iri,
#Context HttpServletRequest request
) {
return service.validate(iri);
}
...
And a custom object mapper configured as (Result class is part of a third-party library)
#Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
SimpleModule module = new SimpleModule();
module.addSerializer(Result.class, new ResultSerializer());
mapper.registerModule(module);
return mapper;
}
Inside ResultSerializer (my custom implementation) I need to customize the serialization output based on the Accept-language header (e.g. from the bounding HttpServletRequest). I am currently only able to solve by not using Spring injection for the ObjectMapper, explicitly creating an ObjectMapper instance in my controller, executing it and returning the output. So:
Can an Accept-language header be obtained in the ObjectMapper instance injected as a Spring bean?
A possibility (not knowing exactly what you are trying to customize): You could try making your ResultSerializer request-scoped.
#Bean
#RequestScope
public ResultSerializer resultSerializer() {
return new ResultSerializer();
}
Then you can simply inject your HttpServletRequest into the ResultSerializer.
public class ResultSerializer {
#Autowired
private HttpServletRequest request;
}
Thanks to Marco, I managed to adjust slightly his solution to get it working using:
#Bean
#RequestScope
public ObjectMapper objectMapper(#Autowired HttpServletRequest request) {
...
module.addSerializer(Result.class, new ResultSerializer(request.getLocale().toLanguageTag()));
...
}

Jackson: Use default (de)serializer

I'm trying to (de)serialize an object that has a property with a type that comes from a maven dependency, so I can't change the class of this type.
The class of this type has a #JsonSerialize and #JsonDeserialize annotation.
However, I want to use the default serializer and deserialzer, because the custom serializer writes an array instead of an object. Is there a way, using annotations, to tell jackson to use the default (de)serializer?
You can disable the annotations using Jackson's mixins feature.
In the following example, any attempt at deserializing to a CustomerObj will result in an exception due to its defective Builder:
#JsonDeserialize(builder = CustomerObj.class)
public class CustomerObj {
public String name;
public int age;
public CustomerObj build() {
throw new RuntimeException("JsonDeserializer invoked");
}
}
Create a mixin with a JsonDeserialize annotation that disables the broken builder:
#JsonDeserialize(builder = java.lang.Void.class)
public static abstract class CustomerMixin { }
Register the mixin on the ObjectMapper instance:
ObjectMapper om = new ObjectMapper();
om.addMixIn(CustomerObj.class, CustomerMixin.class);
Enjoy working deserialization:
final String json = "{\"name\":\"Brian\",\"age\":41}";
CustomerObj customer = om.readValue(json, CustomerObj.class);

Registering JacksonJsonProvider with ObjectMapper + JavaTimeModule to Jersey 2 Client

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>>(){});
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:
Disable the auto-discovery (as mention in the previous link)1
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.
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
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;
}
}
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.

Categories

Resources