Spring Boot doesn't use custom deserializer for ZonedDateTime - java

I am trying to setup my spring boot configuration to use my custom serializer and deserializer.
Deserializer code
public class CustomZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
#Override
public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
ObjectCodec oc = jsonParser.getCodec();
TextNode node = (TextNode) oc.readTree(jsonParser);
String dateString = node.textValue();
return ZonedDateTime.parse(dateString, CustomZonedDateTimeSerializer.formatter);
}
#Override
public Class<?> handledType() {
return ZonedDateTime.class;
}
}
Serializer code
public class CustomZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
#Override
public Class<ZonedDateTime> handledType() {
return ZonedDateTime.class;
}
public final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
#Override
public void serialize(ZonedDateTime date, JsonGenerator generator, SerializerProvider provider)
throws IOException, JsonProcessingException {
final String dateString = date.format(formatter);
generator.writeString(dateString);
}
}
This my configuration:
#Configuration
public class JacksonConfiguration {
#Autowired
Jackson2ObjectMapperBuilder jacksonBuilder;
#PostConstruct
public void configureJackson() {
final CustomZonedDateTimeSerializer serializer = new CustomZonedDateTimeSerializer();
jacksonBuilder.serializers(serializer);
final CustomZonedDateTimeDeserializer deserializer = new CustomZonedDateTimeDeserializer();
jacksonBuilder.deserializers(deserializer);
}
}
I am creating a REST API so I have this in my controller:
#RequestMapping(value = "date", method = { RequestMethod.GET })
#ApiOperation(value = "", notes = "")
public ResponseWrapper<String> testDATE(
#RequestParam #ApiParam(value = "", required = true) ZonedDateTime date) {
System.out.println(date.toString());
return new ResponseWrapper<String>(date.toString());
}
So far I have tried:
creating a custom module and adding it as a #Bean in the configuration, adding the serializer and deserializer to the jackonBuilder object in configuration and something like the code in this example
Am I missing something or doing something wrong?
Each time I try I get the following error:
"Failed to convert value of type 'java.lang.String' to required type
'java.time.ZonedDateTime'; nested exception is
org.springframework.core.convert.ConversionFailedException: Failed to
convert from type [java.lang.String] to type
[#org.springframework.web.bind.annotation.RequestParam
#io.swagger.annotations.ApiParam java.time.ZonedDateTime] for value
'2018-07-10T00:00:00+0000'; nested exception is
java.lang.IllegalArgumentException: Parse attempt failed for value
[2018-07-10T00:00:00+0000]"
I am sending the following value each time: 2018-07-10T00:00:00+0000 and the value doesn't even reach the deserializer where I have my breakpoint.

Add this bean in your configuration class:
#Bean
public Jackson2ObjectMapperBuilder configureObjectMapper() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
final CustomZonedDateTimeSerializer serializer = new CustomZonedDateTimeSerializer();
builder.serializers(serializer);
final CustomZonedDateTimeDeserializer deserializer = new CustomZonedDateTimeDeserializer();
builder.deserializers(deserializer);
return builder;
}

Seems like I could not send ZonedDateTime in GET requests. Once I changed to POST, Spring was able to parse the date automatically.
I only added spring.jackson.deserialization.adjust_dates_to_context_time_zone=true so it takes the timezone into consideration.
If someone will want to send it in GET he will have to send a string and parse that instead of defining ZonedDateTime as a parameter.

Related

Custom serialization without annotations

Is there a way on Springboot that I can implement a custom serializer for a specific field on my request without doing annotations?
I prefer if we could create a bean or a override a configuration and serialize a string input (from json request) going to OffsetDateTime field on my request pojo.
I cannot annotate because my request classes are auto-generated..
You can register the serializer programatically in jackson. The class needing custom serialization:
public class SpecialObject {
private String field;
private String anotherField;
//getters, setters
}
The wrapper class, where it is a field:
public class WrapperObject {
private String text;
private SpecialObject specialObject;
//getters, setters
}
The serializer:
public class SpecialObjectSerializer extends StdSerializer<SpecialObject> {
public SpecialObjectSerializer() {
super(SpecialObject.class);
}
#Override
public void serialize(SpecialObject value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("fieldChanged", value.getField());
gen.writeStringField("anotherFieldChanged", value.getAnotherField());
gen.writeEndObject();
}
}
Nothing fancy, just changing field names when serializing.
Now you need to add your serializer in a Module and register that module in object mapper. You can do it like this:
public class NoAnnot {
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
//add serializer in module
module.addSerializer(SpecialObject.class, new SpecialObjectSerializer());
//register module
objectMapper.registerModule(module);
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
SpecialObject specialObject = new SpecialObject();
specialObject.setField("foo");
specialObject.setAnotherField("bar");
WrapperObject wrapperObject = new WrapperObject();
wrapperObject.setText("bla");
wrapperObject.setSpecialObject(specialObject);
String json = objectMapper.writeValueAsString(wrapperObject);
System.out.println(json);
}
}

Formatter<LocalDateTime> not getting registered with Spring Boot container in REST application

I have implemented a Formatter in a REST Spring Boot web service application to format all LocalDateTime (Java 8) attributes so that they get displayed in a specific format in the web service response.
public class LocalDateTimeFormatter implements Formatter<LocalDateTime> {
#Override
public String print(LocalDateTime temporal, Locale locale) {
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss");
return formatter.format(temporal);
}
#Override
public LocalDateTime parse(String text, Locale locale)
throws ParseException {
return LocalDateTime.parse(text);
}
}
The formatter has also been registered as shown below so that it works at the application level in order to avoid annotating all LocalDateTime attributes individually
#Configuration
public class ContentFormatter implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new LocalDateTimeFormatter());
}
}
But no formatting is applied to the response from the web service, accessDate being the LocalDateTime type of attribute
{
"content" : "Hello, World!",
"id" : 0,
"accessDate" : "2020-04-07T19:56:41.48"
}
You probably don't even need to implement your formatted and register it. Just annotate your LocalDateTime property as follows:
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
public LocalDateTime getTime() {
return time;
}
Also, you might need to add the following dependency:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.6.0</version>
</dependency>
See this question and its answer for details:
Spring Data JPA - ZonedDateTime format for json serialization
You can use StdSerializer
public class JacksonLocalDateSerializer extends StdSerializer<LocalDate> {
private static final long serialVersionUID = -7880057299936771237L;
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withResolverStyle(ResolverStyle.STRICT);
public JacksonLocalDateSerializer() {
this(null);
}
public JacksonLocalDateSerializer(Class<LocalDate> type) {
super(type);
}
#Override
public void serialize(LocalDate value, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeString(formatter.format(value));
}
}
Then add configuration for the serializer for applicable the entire application
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper configureObjectMapper() {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new JacksonLocalDateTimeSerializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(javaTimeModule);
return mapper;
}
}

Configure JavaTimeModule in ObjectMapper globally instead of using #datetimeformat

Hi I try to create a controller which will accept request-parameter as LocalDateTime.
ex: /api/actions?page=0&size=10&from=2018-05-02T20:20:20&to=2018-06-02T20:20:20
at controller if I using code bellow it work:
#RequestParam(value = "from")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
LocalDateTime from,
#RequestParam(value = "to")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
LocalDateTime to
But I want to move #DateTimeFormat to globally configuration, and I choose ObjectMapper:
I create a bean in configuration:
#Bean
public ObjectMapper jacksonObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(
LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
And try
#Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(
LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
This is dateTimeFormat value: yyyy-MM-dd'T'HH:mm:ss.SS
Both of 2 ways above are not working, It said:
class org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#org.springframework.web.bind.annotation.RequestParam java.time.LocalDateTime] for value '2018-05-02T20:20:20'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2018-05-02T20:20:20]
My jackson version:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.8.8</version>
</dependency>
Am I missing something?
Thank you for your time.
class org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime'; nested exception is org.springframework.core.convert.ConversionFailedException
I think here you need to use JsonSerializer and JsonDeserializer.
So, when request comes you use JsonDeserializer, it will convert your date of String format to required Date format. Here is a code,
#Component
public class DateDeSerializer extends JsonDeserializer<Date> {
public final SimpleDateFormat formatter = new SimpleDateFormat("date format");
#Override
public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.getCurrentToken().equals(JsonToken.VALUE_STRING)) {
try {
return formatter.parse(jp.getText());
} catch (ParseException e) {
// throw exception
}
}
return null;
}
#Override
public Class<Date> handledType() {
return Date.class;
}
}
To format your response use JsonSerializer. Here is a sample code,
#Component
public class DateSerializer extends JsonSerializer<Date> {
DateFormat formatter = new SimpleDateFormat("date format");
#Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeObject(formatter.format(value));
}
#Override
public Class<Date> handledType() {
return Date.class;
}
}
Problem is I pass LocalDateTime at requestParam, but I using ObjectMapper which only work with request's body.
And to resolve my issue I create new component LocalDateTimeConverter and remove bean of ObjectMapper.
#Component
public class LocalDateTimeConverter implements Converter<String, LocalDateTime> {
private final DateTimeFormatter formatter;
#Autowired
public LocalDateTimeConverter(#Value("${dateTime.format}") String dateTimeFormat) {
this.formatter = DateTimeFormatter.ofPattern(dateTimeFormat);
}
#Override
public LocalDateTime convert(String source) {
if (source == null || source.isEmpty()) {
return null;
}
return LocalDateTime.parse(source, formatter);
}
}

Jackson JsonDeserialize not being called for #QueryParam

I have mapped a custom deserializer to convert Strings on dd/MM/yyyy pattern to LocalDate so I can call my services with a more readable signature..
This is my dto class that is used as a Jersey #BeanParam to transport data between layers:
public class ProdutoFilterDto implements FilterDto {
private static final long serialVersionUID = -4998167328470565406L;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataInicial;
#QueryParam("dataInicial")
#JsonDeserialize(using = CustomLocalDateDeserializer.class)
private LocalDate dataFinal;
public LocalDate getDataInicial() {
return dataInicial;
}
public void setDataInicial(LocalDate dataInicial) {
this.dataInicial = dataInicial;
}
public LocalDate getDataFinal() {
return dataFinal;
}
public void setDataFinal(LocalDate dataFinal) {
this.dataFinal = dataFinal;
}
}
and this is my custom deserializer:
public class CustomLocalDateDeserializer extends JsonDeserializer<LocalDate> {
#Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
final String data = p.getValueAsString();
return (LocalDate) formatter.parse(data);
}
}
Its being used on this jersey service:
#Path("produto")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
public class ProdutoService {
...
#GET
#Path("query")
#Override
public Response query(
#QueryParam("offset") #DefaultValue(value = "0") Integer offSet,
#QueryParam("limit") #DefaultValue(value = "10") Integer limit,
#BeanParam ProdutoFilterDto filter) { ... }
...
}
I am calling like this:
${host goes here}/produto/query?dataInicial=11/09/1992
The problem is that the deserializer method is never called and the bean param variable remains null..
MessageBodyReaders aren't used for #QueryParam. You seem to be expecting the Jackson MessageBodyReader to handle this deserialization, but it doesn't work like that.
Instead you will want to use a ParamConverter, which will need to be registered through a ParamConverterProvider. For example:
#Provider
public class LocalDateParamConverterProvider implements ParamConverterProvider {
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
#Override
public <T> ParamConverter<T> getConverter(
Class<T> rawType, Type genericType, Annotation[] antns) {
if (LocalDate.class == rawType) {
return new ParamConverter<T>() {
#Override
public T fromString(String string) {
try {
LocalDate localDate = LocalDate.parse(string, formatter);
return rawType.cast(localDate);
} catch (Exception ex) {
throw new BadRequestException(ex);
}
}
#Override
public String toString(T t) {
LocalDate localDate = (LocalDate) t;
return formatter.format(localDate);
}
};
}
return null;
}
}
Now LocalDate will work with #QueryParam and other #XxxParams also.
Some things to note:
If your goal is to parse both your #XxxParams and your JSON body into a bean this will not work. I'm not sure how that would work, but I'm sure it would involve a lot of hacking, and I wouldn't recommend it.
Your cast to (LocalDate) won't work. It's an illegal cast to java.time.format.Parsed. See correct way in code example.
Related to the above point. I was pulling out my hair for a good hour trying to figure out why I was getting a 404, using your parse code. With a 404, the last place I thought to look was in the ParamConverter. But it seems any uncaught exceptions that are thrown in the ParamConverter, will cause a 404. Doesn't make much sense right? The head pounding led me to this, which led me to this, which seems to be a poor specification
"if the field or property is annotated with
#MatrixParam, #QueryParam or #PathParam then an implementation MUST generate an instance of
NotFoundException (404 status) that wraps the thrown exception and no entity
"
Moral of the story: make sure to catch any possible exceptions in the ParamConverter!
See Also:
Good article on ParamConverters

FasterXml Jackson serialize properly but doesn't call deserialize on ZonedDateTime

I'm on a project that require FasterXML and ZonedDateTime. It is mandatory to not use annotation like using=Deserializer.class because we want to keep the same format of date on all the project.
That being said, I come here to ask help because I can't figure out why my Deserializer is instanciate but its method deserialize() is not call ...
Here my configuration :
The serializer :
public class ZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
/**
* The date time formatter to use
*/
DateTimeFormatter dtf;
/**
* Instanciate a new serializer
*
* #param dtf The datetime formatter
*/
public ZonedDateTimeSerializer(DateTimeFormatter dtf) {
System.out.println("ZonedDateTimeSerializer()");
this.dtf = dtf;
}
#Override
public void serialize(ZonedDateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
System.out.println("ZonedDateTimeSerializer::serialize()");
if (null != dateTime) {
jsonGenerator.writeString(dateTime.format(dtf));
}
}
}
The deserializer :
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
/**
* The date time formatter to use
*/
DateTimeFormatter dtf;
/**
* Instanciate a new deserializer
*
* #param dtf The datetime formatter
*/
public ZonedDateTimeDeserializer(DateTimeFormatter dtf) {
Preconditions.checkNotNull(dtf, "Date time formatter is null");
System.out.println("ZonedDateTimeDeserializer()");
this.dtf = dtf;
}
#Override
public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
System.out.println("ZonedDateTimeDeserializer::deserialize()");
return ZonedDateTime.parse(jsonParser.getText(), dtf);
}
}
This two add to the mapper in a mapper provider :
#Provider
public class JacksonConfigurator implements ContextResolver<ObjectMapper> {
private ObjectMapper mapper = new ObjectMapper();
/**
* Constructor
*/
public JacksonConfigurator() {
System.out.println("new ObjectMapperResolver()");
DateTimeFormatter sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
SimpleModule xxxModule = new SimpleModule("XXXX", new Version(2, 0, 0, null))
.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer(sdf))
.addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer(sdf));
mapper.registerModule(xxxModule);
}
#Override
public ObjectMapper getContext(Class<?> arg0) {
return mapper;
}
}
And register here in a glassfish jersey ResourceConfig;
#ApplicationPath("api")
public class ApplicationRessourceConfig extends ResourceConfig {
/**
* Define application config
*/
public ApplicationRessourceConfig() {
// File to parse
packages(true, "com.xxx.xxx");
// CDI to Jersey
register(new WebServiceBinder());
// Jackson config
register(new JacksonConfigurator());
// Role
register(RolesAllowedDynamicFeature.class);
}
}
I can't test it in production or development until the tests are ok. So I make the test like :
public class xxxTest extends JerseyTest {
...
#Override
protected Application configure() {
...
ResourceConfig config = new ResourceConfig(...);
config.register(new JacksonConfigurator());
return config;
}
#Test
public void xxx() {
...
response.bufferEntity();
response.readEntity(EntityWithZonedDateTime.class);
...
}
}
When I was using the annotation #JsonDeserializer on each ZonedDateTime property it worked perfectly but now, the Serializer constructor is logged, serialize() is logged too, event the Deserializer constructor is logged, but not the deserialize() method.
Instead I have :
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class java.time.ZonedDateTime] from String value ('2012-06-30T12:30:40.000+0000'); no single-String constructor/factory method
at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream#3af4e0bf; line: 1, column: 46] (through reference chain: com.xxx.xxx.resources.utils.Xxxx["beginDate"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)
at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:843)
at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:277)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:284)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1150)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:153)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:144)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:523)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:101)
at com.fasterxml.jackson.databind.deser.impl.BeanPropertyMap.findDeserializeAndSet(BeanPropertyMap.java:285)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:248)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:136)
at com.fasterxml.jackson.databind.ObjectReader._bind(ObjectReader.java:1408)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:858)
at com.fasterxml.jackson.jaxrs.base.ProviderBase.readFrom(ProviderBase.java:777)
At 2pm, it will be 48 hours I'm on it. Impossible to know why the mapper doesn't try to desiarilize with the deserializer and it fallback on the string. And guess what ... The fasterxml wiki is down ... Any clue is welcome.
--- EDIT ---
For information, the entity sent in request look like :
#XmlRootElement
public class XxxOutput {
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public static class XxxJson {
/* ---------- Properties ---------- */
public BigInteger id;
public String name;
public String definition;
public ZonedDateTime beginDate;
public ZonedDateTime endDate;
public ZonedDateTime creationDate;
public ZonedDateTime lastUpdate;
public Map<String, BigInteger> relateds;
/* ---------- Constructor ---------- */
public XxxJson() {}
public XxxJson(... all args ...) {
this.id = id;
...
this.relateds = relateds;
}
/* ---------- Useful methods ---------- */
public XxxJson addRelated(final Xxx related) {
...
// add the Xxx.id to the list
...
}
#Override
public String toString() {
return id.toString();
}
}
}

Categories