Ash Ash - 29 days ago 14
Java Question

Cannot deserialize custom Date object with Jackson in Springboot "no single-String constructor/factory method"

Edit:

In an effort to clarify the issue I've made a diagram:

enter image description here

I've been migrating custom Jetty web services to use the Spring REST Templates and I've ran into an issue with the following deserializer.

The custom-date format comes back like so:

"2016-10-18"
and I need to transform that into 3 integers before calling a constructor of this custom
Date
type.

Previously this was working because we just passed the service an
ObjectMapper
with the deserializer registered and it called
deserialize
with no issues.

Now however I'm getting an error because Jackson attempts to call a
String
constructor on my custom
Date
type which doesn't exist. This library is not maintained by me and therefore I cannot add this constructor.

Error:


"Failed to read HTTP message:
org.springframework.http.converter.HttpMessageNotReadableException:
Could not read document: Can not instantiate value of type [simple
type, class com.intercorp.domain.date.Date] from String value
('2016-09-30'); no single-String constructor/factory method


Note



Annotating the attribute that I am attempting to serialize works, but I do not consider this to be a solution because registering the deserialiser should detect the type and call itself. I'm more interested in why this is not working than switching to annotations:

@JsonDeserialize(using=JacksonConfig.CustomDateDeserializer.class)
private Date paymentDate;


I have the following Spring config:

@Import({JacksonConfig.class})
public class WebServiceConfig {

@Autowired
private MappingJackson2HttpMessageConverter jacksonConverter;

@Bean
public MyWebService MyWebService () {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(jacksonConverter);
return new MyWebService(restTemplate);
}
}


JacksonConfig.class

@Configuration
public class JacksonConfig {

@Bean
public MappingJackson2HttpMessageConverter jacksonConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(objectMapper());
return converter;
}

@Bean
@Primary
public ObjectMapper objectMapper() {
Jackson2ObjectMapperBuilder mapperBuilder = new Jackson2ObjectMapperBuilder();
mapperBuilder.modules(myModule()).timeZone(TimeZone.getTimeZone("UTC"))
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapperBuilder.build();
}

@Bean
public Module myModule() {
SimpleModule module = new SimpleModule("myModule", new Version(0, 0, 1, null));

module.addSerializer(new CustomDateSerializer());
//not Java Date - custom Date...
module.addDeserializer(Date.class, new CustomDateDeserializer());

return module;
}

// breakpoints indicate this is never called
static public class CustomDateDeserializer extends JsonDeserializer<Date> {

@Override
@SneakyThrows
public Date deserialize(JsonParser jp, DeserializationContext ctxt) {
String text = jp.getText();
if (text == null) {
return null;
}
if (text.length() >= 10) {
int year = parseInt(text.substring(0, 4));
int month = parseInt(text.substring(5, 7));
int day = parseInt(text.substring(8, 10));
return new Date(year, month, day);
}
// deserialize json object
else if (JsonToken.START_OBJECT.equals(jp.getCurrentToken())) {
JsonNode dateNode = jp.getCodec().readTree(jp);
return getDate(dateNode);
}
else {
return new Date(Integer.parseInt(text));
}
}
}

}


PaymentStatus.java

package com.otpp.memberapi.service.iaccess.data.pension;

import com.intercorp.domain.date.Date;
import com.intercorp.domain.money.Money;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PensionPaymentStatus {

private boolean available;
private Money netMonthly;
//this date fails to deserialise
private Date paymentDate;
private boolean suspended;

}


Example JSON payload

{
"available": true,
"netMonthly": "1000.00",
"paymentDate": "2016-10-19",
"suspended": false
}


The date class is huge and just wraps Joda time (used in pre-Java8 envs). It has several constructors, the one I'm trying to use is a YYYY, MM, DD integer constructor.

Answer

It seems like request is not being deserialized by your converter.

RestTemplate`s no-arg constructor initializes it with default converters, relevant snippet:

....
if (jackson2Present) {
    this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
....

And since it uses 'first one wins' strategy to find appropriate converter it will never use yours because it is positioned later in the list.

You need to change:

RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(jacksonConverter);

To something like this:

RestTemplate restTemplate = new RestTemplate(Collections.singletonList(jacksonConverter));

Depending on whether you need additional converters or not.