I am working on a Java servlet and I need to serialize and deserialize a class into JSON and back. To do this, I am using the Genson library, but I am hitting a snag.
Genson fails at deserializing instances of the Date class (java.sql.Date)
I have tried setting custom date formatters, but they don't seem to affect deserialization.
I have also tried to plug in new converters using the builder call withConverter(), but I can't figure out how the parameters work.
Here is my builder call for the moment
Genson genson = builder.setSkipNull(true).create();
The class I am serializing has a field of type Date
private Date introDate;
This is a snippet of the stack trace that occurs if I try to deserialize the produced JSON
Caused by: com.owlike.genson.JsonBindingException: Could not access value of property named 'hours' using accessor public int java.sql.Date.getHours() from class java.sql.Date
at com.owlike.genson.reflect.PropertyAccessor.couldNotAccess(PropertyAccessor.java:40)
at com.owlike.genson.reflect.PropertyAccessor$MethodAccessor.access(PropertyAccessor.java:70)
at com.owlike.genson.reflect.PropertyAccessor.serialize(PropertyAccessor.java:24)
at com.owlike.genson.reflect.BeanDescriptor.serialize(BeanDescriptor.java:92)
at com.owlike.genson.convert.NullConverterFactory$NullConverterWrapper.serialize(NullConverterFactory.java:69)
at com.owlike.genson.reflect.PropertyAccessor.serialize(PropertyAccessor.java:27)
... 38 more
The problem here is that methods like getHours() are deprecated, thus calling them produces an IllegalArgumentException. I do not know how to get around this for the moment.
You could use java.util.Date. That works with Genson.
If we're sticking with java.sql.Date, then you can write your own converter and make Genson use that.
Let's start with an object to ser/deser:
import java.sql.Date;
import lombok.Getter;
import lombok.Setter;
public class CrashTestDummy {
#Getter #Setter private String name;
#Getter #Setter private Date sqlDate;
/** Default no-arg constructor */
public CrashTestDummy() {
}
}
Then we can tell Genson to use a custom converter. In this case I'm converting it to a Long in epoch time and back again. You might decide to use a specific date format instead.
import java.sql.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import org.junit.Test;
import com.owlike.genson.Context;
import com.owlike.genson.Converter;
import com.owlike.genson.Genson;
import com.owlike.genson.GensonBuilder;
import com.owlike.genson.stream.ObjectReader;
import com.owlike.genson.stream.ObjectWriter;
#Test
public void serialiseDate() throws ParseException {
// create a converter for java.sql.Date
Converter<Date> converter = new Converter<Date>() {
#Override
public void serialize(Date obj, ObjectWriter writer, Context ctx) throws Exception {
// java.sql.Date doesn't support any time fields, so we can just focus on y, M and d
if(obj == null) {
writer.writeNull();
return;
}
writer.writeValue( obj.getTime() );
}
#Override
public Date deserialize(ObjectReader reader, Context ctx) {
Long value = reader.valueAsLong();
Date sqlDate = new java.sql.Date( value );
return sqlDate;
}
};
// Build a new Genson object with our converter
Genson genson = new GensonBuilder()
.setSkipNull(true)
.withConverter(converter, java.sql.Date.class)
.create();
// make an important SQL date for testing
java.util.Date utilDate = new SimpleDateFormat("dd MMM yyyy").parse("30 July 1966");
Date sqlDate = new Date(utilDate.getTime() );
// Make something to serialise
CrashTestDummy original = new CrashTestDummy();
original.setName( "Alfa Khrisna" );
original.setSqlDate( sqlDate );
// Call Genson as usual
String json = genson.serialize( original );
System.out.println( json );
// Deserialise as usual; for brevity I'm comparing dates in millis since epoch.
CrashTestDummy clone = genson.deserialize(json, CrashTestDummy.class);
assertEquals(new SimpleDateFormat("dd MMM yyyy").parse("30 July 1966").getTime(), clone.getSqlDate().getTime());
assertEquals("Alfa Khrisna", clone.getName());
}
I am working on custom JSON deserialiser and have the below class
public class yyyy_MM_dd_DateDeserializer extends StdDeserializer <LocalDate> {
public yyyy_MM_dd_DateDeserializer() {
this(null);
}
public yyyy_MM_dd_DateDeserializer(Class t) {
super(t);
}
#Override
public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String dateString = jsonParser.getText();
LocalDate localDate = null;
try {
localDate = LocalDate.parse(dateString, "yyyy-MM-dd");
} catch (DateTimeParseException ex) {
throw new RuntimeException("Unparsable date: " + dateString);
}
return localDate;
}
}
and in my request class
#Valid
#JsonDeserialize(using = LocalDateDeserializer.class)
#JsonSerialize(using = LocalDateSerializer.class)
private LocalDate endDate;
It works fine but I am wondering if I can pass the date format dynamically. Instead of hardcoding in yyyy_MM_dd_DateDeserializer.
I want to pass the date format from my request class so that my deserialiser is more generic any anyone can use it by sending the required format.
I think you working too hard to get what you want. There is a simpler way without writing your own deserializer. Look at this question. Essentially it looks like
#JsonFormat(shape= JsonFormat.Shape.STRING, pattern="EEE MMM dd HH:mm:ss Z yyyy")
#JsonProperty("created_at")
ZonedDateTime created_at;
And you just put your own mask. Also, I once had a task of parsing date with unknown format, essentially I needed to parse any valid date. Here is an article describing the idea of how to implement it: Java 8 java.time package: parsing any string to date. You might find it useful
Not when using a binder library (The very point of binding is that it is not dynamic.).
But you could when using a simple parsing library such as org.json
When you are working with java.time.* classes and Jackson is good to start from registering JavaTimeModule which comes from jackson-datatype-jsr310 module. We can extend it and register serialiser with provided pattern like in below example:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class JsonApp {
public static void main(String[] args) throws Exception {
ObjectMapper mapperIso = createObjectMapper("yyyy-MM-dd");
ObjectMapper mapperCustom0 = createObjectMapper("yyyy/MM/dd");
ObjectMapper mapperCustom1 = createObjectMapper("MM-dd-yyyy");
System.out.println(mapperIso.writeValueAsString(new Time()));
System.out.println(mapperCustom0.writeValueAsString(new Time()));
System.out.println(mapperCustom1.writeValueAsString(new Time()));
}
private static ObjectMapper createObjectMapper(String pattern) {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(pattern)));
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(javaTimeModule);
return mapper;
}
}
class Time {
private LocalDate now = LocalDate.now();
public LocalDate getNow() {
return now;
}
public void setNow(LocalDate now) {
this.now = now;
}
#Override
public String toString() {
return "Time{" +
"now=" + now +
'}';
}
}
Aboce code prints:
{"now":"2019-02-24"}
{"now":"2019/02/24"}
{"now":"02-24-2019"}
I have an issue where my RestTemplate.postForEntity(url, restRequest, RepoResponse.class) call is failing because it can't deserialise dates of the form:
2019-02-01T12:00:00.000-0500
because of the missing colon in the timezone.
Based on this answer, it looks like I want to change the date formatting of my RestTemplate's ObjectMapper.
I've tried a solution here:
https://stackoverflow.com/a/38286322/14250
Which gives me the following code:
restTemplate = new RestTemplate();
ObjectMapper objectMapper = new ObjectMapper();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS[XXX][X]");
objectMapper.setDateFormat(format);
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setPrettyPrint(false);
messageConverter.setObjectMapper(objectMapper);
restTemplate.getMessageConverters().removeIf(m->m.getClass().getName().equals(MappingJackson2HttpMessageConverter.class.getName()));
restTemplate.getMessageConverters().add(messageConverter);
Unfortunately I get the following error
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.OffsetDateTime` (no Creators, like default construct, exist): no String-argument constructor/factory method to deserialize from String value ('2019-02-13T12:33:19.179-0500')
Is there an easy way to make RestTemplate support the above datetime format?
When you are working with java.time.* classes and Jackson is good to start from registering JavaTimeModule which comes from jackson-datatype-jsr310 module.
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.time.OffsetDateTime;
public class JsonApp {
public static void main(String[] args) throws Exception {
JavaTimeModule javaTimeModule = new JavaTimeModule();
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(javaTimeModule);
String json = "{\"now\":\"2019-02-01T12:01:01.001-0500\"}";
System.out.println(mapper.readValue(json, Time.class));
}
}
class Time {
#JsonFormat(pattern = "uuuu-MM-dd'T'HH:mm:ss.SSSX")
private OffsetDateTime now = OffsetDateTime.now();
public OffsetDateTime getNow() {
return now;
}
public void setNow(OffsetDateTime now) {
this.now = now;
}
#Override
public String toString() {
return "PrintObject{" +
"now=" + now +
'}';
}
}
Above code prints:
PrintObject{now=2019-02-01T17:01:01.001Z}
I am trying to serialize Java objects to JSON. One of my Java objects has a JodaTime LocalTime object as one of its fields.
A fair number of my Java objects also have various fields that are Collections that could be empty. I want to prevent the serialization of JSON that looks like this:
{id: 2348904, listOfThings: [], listOfStuff: [], nowASet: []}
In this scenario where those three Collections are empty, I would rather see this JSON:
{id: 2348904}
The correct way to do such a thing is to configure the ObjectMapper with the following line of code:
objectMapper.setSerializationInclusion(Include.NON_EMPTY);
This works just fine...until I hit that Java object with the LocalTime inside of it. That's when I get an actual java.lang.StackOverflowError.
It seems to be ping-ponging between JodaDateSerializerBase.isEmpty() and JsonSerializer.isEmpty(). I'm not sure how, though, because they don't call each other.
I managed to make a SSSSSSCCCCEEEE, or whatever the hell the acronym is, as follows:
package whatever.you.like;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import org.joda.time.LocalTime;
import org.junit.Test;
public class TestClass {
public class JodaMapper extends ObjectMapper {
private static final long serialVersionUID = 34785437895L;
public JodaMapper() {
registerModule(new JodaModule());
}
public boolean getWriteDatesAsTimestamps() {
return getSerializationConfig().isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
public void setWriteDatesAsTimestamps(boolean state) {
configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, state);
}
}
private class Thing {
private LocalTime localTime;
public Thing() {}
public void setLocalTime(LocalTime localTime) {
this.localTime = localTime;
}
public LocalTime getLocalTime() {
return localTime;
}
}
#Test
public void extendObjectMapperTest() throws JsonProcessingException {
JodaMapper objectMapper = new JodaMapper();
objectMapper.setWriteDatesAsTimestamps(false);
objectMapper.setSerializationInclusion(Include.NON_EMPTY);
Thing thing = new Thing();
LocalTime localTime = new LocalTime(12389340L);
thing.setLocalTime(localTime);
System.out.println("Never manages to print this out: " + objectMapper.writeValueAsString(thing));
}
#Test
public void configureObjectMapperTest() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JodaModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.setSerializationInclusion(Include.NON_EMPTY);
Thing thing = new Thing();
LocalTime localTime = new LocalTime(12389340L);
thing.setLocalTime(localTime);
System.out.println("Never manages to print this out: " + objectMapper.writeValueAsString(thing));
}
}
I tried both extending the ObjectMapper and configuring the ObjectMapper, and I get the same error each time.
Dependencies:
JodaTime 2.6
FasterXML's Jackson 2.5.0
FasterXML's Jackson-DataType-Joda 2.5.0
Interestingly, you can find in that GitHub a unit test ("testLocalDateSer()") that claims to succeed using the Include.NON_EMPTY qualifier. I fail to see how it could possibly function.
Upgrade to
FasterXML's Jackson 2.5.3
FasterXML's Jackson-DataType-Joda 2.5.3.
This works.
#Test
public void configureObjectMapperTest() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JodaModule());
// objectMapper.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
objectMapper.setSerializationInclusion(Include.NON_EMPTY);
Thing thing = new Thing();
LocalTime localTime = new LocalTime(12389340L);
thing.setLocalTime(localTime);
System.out.println("Never manages to print this out: " + objectMapper.writeValueAsString(thing));
}
I have a Date format coming from API like this:
"start_time": "2015-10-1 3:00 PM GMT+1:00"
Which is YYYY-DD-MM HH:MM am/pm GMT timestamp.
I am mapping this value to a Date variable in POJO. Obviously, its showing conversion error.
I would like to know 2 things:
What is the formatting I need to use to carry out conversion with Jackson? Is Date a good field type for this?
In general, is there a way to process the variables before they get mapped to Object members by Jackson? Something like, changing the format, calculations, etc.
Since Jackson v2.0, you can use #JsonFormat annotation directly on Object members;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm a z")
private Date date;
What is the formatting I need to use to carry out conversion with Jackson? Is Date a good field type for this?
Date is a fine field type for this. You can make the JSON parse-able pretty easily by using ObjectMapper.setDateFormat:
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm a z");
myObjectMapper.setDateFormat(df);
In general, is there a way to process the variables before they get mapped to Object members by Jackson? Something like, changing the format, calculations, etc.
Yes. You have a few options, including implementing a custom JsonDeserializer, e.g. extending JsonDeserializer<Date>. This is a good start.
Of course there is an automated way called serialization and deserialization and you can define it with specific annotations (#JsonSerialize,#JsonDeserialize) as mentioned by pb2q as well.
You can use both java.util.Date and java.util.Calendar
... and probably JodaTime as well.
The #JsonFormat annotations not worked for me as I wanted (it has adjusted the timezone to different value) during deserialization (the serialization worked perfect):
#JsonFormat(locale = "hu", shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "CET")
#JsonFormat(locale = "hu", shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Europe/Budapest")
You need to use custom serializer and custom deserializer instead of the #JsonFormat annotation if you want predicted result. I have found real good tutorial and solution here http://www.baeldung.com/jackson-serialize-dates
There are examples for Date fields but I needed for Calendar fields so here is my implementation:
The serializer class:
public class CustomCalendarSerializer extends JsonSerializer<Calendar> {
public static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public static final Locale LOCALE_HUNGARIAN = new Locale("hu", "HU");
public static final TimeZone LOCAL_TIME_ZONE = TimeZone.getTimeZone("Europe/Budapest");
#Override
public void serialize(Calendar value, JsonGenerator gen, SerializerProvider arg2)
throws IOException, JsonProcessingException {
if (value == null) {
gen.writeNull();
} else {
gen.writeString(FORMATTER.format(value.getTime()));
}
}
}
The deserializer class:
public class CustomCalendarDeserializer extends JsonDeserializer<Calendar> {
#Override
public Calendar deserialize(JsonParser jsonparser, DeserializationContext context)
throws IOException, JsonProcessingException {
String dateAsString = jsonparser.getText();
try {
Date date = CustomCalendarSerializer.FORMATTER.parse(dateAsString);
Calendar calendar = Calendar.getInstance(
CustomCalendarSerializer.LOCAL_TIME_ZONE,
CustomCalendarSerializer.LOCALE_HUNGARIAN
);
calendar.setTime(date);
return calendar;
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
and the usage of the above classes:
public class CalendarEntry {
#JsonSerialize(using = CustomCalendarSerializer.class)
#JsonDeserialize(using = CustomCalendarDeserializer.class)
private Calendar calendar;
// ... additional things ...
}
Using this implementation the execution of the serialization and deserialization process consecutively results the origin value.
Only using the #JsonFormat annotation the deserialization gives different result I think because of the library internal timezone default setup what you can not change with annotation parameters (that was my experience with Jackson library 2.5.3 and 2.6.3 version as well).
To add characters such as T and Z in your date
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
private Date currentTime;
output
{
"currentTime": "2019-12-11T11:40:49Z"
}
Just a complete example for spring boot application with RFC3339 datetime format
package bj.demo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import java.text.SimpleDateFormat;
/**
* Created by BaiJiFeiLong#gmail.com at 2018/5/4 10:22
*/
#SpringBootApplication
public class BarApp implements ApplicationListener<ApplicationReadyEvent> {
public static void main(String[] args) {
SpringApplication.run(BarApp.class, args);
}
#Autowired
private ObjectMapper objectMapper;
#Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"));
}
}
Building on #miklov-kriven's very helpful answer, I hope these two additional points of consideration prove helpful to someone:
(1) I find it a nice idea to include serializer and de-serializer as static inner classes in the same class. NB, using ThreadLocal for thread safety of SimpleDateFormat.
public class DateConverter {
private static final ThreadLocal<SimpleDateFormat> sdf =
ThreadLocal.<SimpleDateFormat>withInitial(
() -> {return new SimpleDateFormat("yyyy-MM-dd HH:mm a z");});
public static class Serialize extends JsonSerializer<Date> {
#Override
public void serialize(Date value, JsonGenerator jgen SerializerProvider provider) throws Exception {
if (value == null) {
jgen.writeNull();
}
else {
jgen.writeString(sdf.get().format(value));
}
}
}
public static class Deserialize extends JsonDeserializer<Date> {
#Overrride
public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws Exception {
String dateAsString = jp.getText();
try {
if (Strings.isNullOrEmpty(dateAsString)) {
return null;
}
else {
return new Date(sdf.get().parse(dateAsString).getTime());
}
}
catch (ParseException pe) {
throw new RuntimeException(pe);
}
}
}
}
(2) As an alternative to using #JsonSerialize and #JsonDeserialize annotations on each individual class member you could also consider overriding Jackson's default serialization by applying the custom serialization at an application level, that is all class members of type Date will be serialized by Jackson using this custom serialization without explicit annotation on each field. If you are using Spring Boot for example one way to do this would as follows:
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public Module customModule() {
SimpleModule module = new SimpleModule();
module.addSerializer(Date.class, new DateConverter.Serialize());
module.addDeserializer(Date.class, new Dateconverter.Deserialize());
return module;
}
}
If anyone has problems with using a custom dateformat for java.sql.Date, this is the simplest solution:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(java.sql.Date.class, new DateSerializer());
mapper.registerModule(module);
(This SO-answer saved me a lot of trouble: https://stackoverflow.com/a/35212795/3149048 )
Jackson uses the SqlDateSerializer by default for java.sql.Date, but currently, this serializer doesn't take the dateformat into account, see this issue: https://github.com/FasterXML/jackson-databind/issues/1407 .
The workaround is to register a different serializer for java.sql.Date as shown in the code example.
I want to point out that setting a SimpleDateFormat like described in the other answer only works for a java.util.Date which I assume is meant in the question.
But for java.sql.Date the formatter does not work.
In my case it was not very obvious why the formatter did not work because in the model which should be serialized the field was in fact a java.utl.Date but the actual object ended up beeing a java.sql.Date.
This is possible because
public class java.sql extends java.util.Date
So this is actually valid
java.util.Date date = new java.sql.Date(1542381115815L);
So if you are wondering why your Date field is not correctly formatted make sure that the object is really a java.util.Date.
Here is also mentioned why handling java.sql.Date will not be added.
This would then be breaking change, and I don't think that is warranted. If we were starting from scratch I would agree with the change, but as things are not so much.
Working for me. SpringBoot.
import com.alibaba.fastjson.annotation.JSONField;
#JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
output:
{
"createTime": "2019-06-14 13:07:21"
}
If we are having the spring boot application, then one more option thats simple to implement for app wide configuration is to use below in application properties file. You can customize the format as needed.
spring.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss.SSS
NOTE: If using this solution use the Spring Dependency Injection to get the reference of the ObjectMapper class.
Cons of not using explicit format is sometimes while upgrading the libraries for jackson code breaks because of change in the format for some versions.