Spring REST timezone issue when using XMLGregorianCalendar - java

I have a spring rest service that accepts and gives json output.
#PostMapping(path = "/path", consumes = {"application/json"}, produces = {"application/json"})
public ResponseEntity<RequestData> method(#RequestBody RequestData request){
return request;
}
RequestData contains several dates (XMLGregorianCalendar). I cannot change the type, since it is generated from xsd. To get dates with the original time zones, I used the parameter
spring.jackson.date-format: yyyy-MM-dd'T'HH:mm:ssZ
Request
{
"date1":"2020-02-28T09:26:59+09:00",
"date2":"2020-01-10T12:46:29+04:00",
"date3":"2020-03-15T11:32:43+08:00"
}
From this request, I got an XMLGregorianCalendar with different time zones.
But when sending a response message, the dates are converted to 0 time zone.
Response
{
"date1":"2020-02-28T00:26:59+0000",
"date2":"2020-01-10T08:46:29+0000",
"date3":"2020-03-15T03:32:43+0000"
}
What settings need to be done on jackson to get non-zero time zones in the response? It is necessary that the response time zones returned in the request.
Or maybe jackson does not know how to do this and always converts the date to a single time zone? In that case, which library to use?
Thanks!
Solution
You must create a serializer and deserializer. Then you need to override the existing ObjectMapper.
If only the serializer is overrided, then upon receipt of the data, the time zone will be normalized (reduced to +00:00), therefore it is also necessary to override the deserializer.
Serializer:
public class XMLGCSerializer extends JsonSerializer<XMLGregorianCalendar> {
#Override
public void serialize(XMLGregorianCalendar value,
JsonGenerator gen,
SerializerProvider serializers)
throws IOException {
gen.writeObject(value.toString());
}
}
Deserializer:
public class XMLGCDeserializer extends JsonDeserializer<XMLGregorianCalendar> {
#Override
public XMLGregorianCalendar deserialize(JsonParser parser, DeserializationContext context) throws IOException {
String stringDate = parser.getText();
try {
return DatatypeFactory.newInstance().newXMLGregorianCalendar(stringDate);
} catch (Exception e) {
throw new RuntimeException(e);
//or return null
}
}
}
Override ObjectMapper
#Component
public class JacksonConfig {
private final ObjectMapper objectMapper;
public JacksonConfig() {
objectMapper = new ObjectMapper();
SimpleModule s = new SimpleModule();
s.addSerializer(XMLGregorianCalendar.class, new XMLGCSerializer());
s.addDeserializer(XMLGregorianCalendar.class, new XMLGCDeserializer());
objectMapper.registerModule(s);
}
#Bean
public ObjectMapper getContext() {
return objectMapper;
}
}

You can create a seperate class to handle serialization by yourself. Here is an example:
class XMLGCSerializer extends JsonSerializer<XMLGregorianCalendar> {
#Override
public void serialize(XMLGregorianCalendar value,
JsonGenerator gen,
SerializerProvider serializers)
throws IOException {
gen.writeObject(value.toString());
}
}
Now you just need to annotate your fields in RequestData:
class RequestData{
#JsonSerialize(using = XMLGCSerializer.class)
XMLGregorianCalendar date1;
//...
}

Related

Serialize objects to a map with the object class as the key?

I'm writing a application using Spring boot and jackson for JSON parsing. I need to handle another service which produces JSON like this:
{
"task-id": 5081,
"task-created-on": {
"java.util.Date": 1631022026000
}
}
Notably, certain fields like the date field here are serialized into a map with a single key-value pair, where the key is a java classname and the value is the actual value of the field.
I've been going through the jackson documentation and haven't found anything about this format. Is there a way to configure jackson to produce and parse fields in this format?
At a minimum, I need to handle dates formatted this way. But I believe the service also uses this format for other objects, where the map key will be the name of some arbitrary java class and the value will be a map of its own. So I'd be interested in a solution that handles more than just dates if possible.
It can be easily done with custom serializer in Jackson by following steps.
First, create objects for serialization as follows:
class MyDateObject {
private Date date;
//general getter/setter
}
class Task {
#JsonProperty("task-id")
private int taskId;
#JsonProperty("task-created-on")
private MyDateObject taskCreatedOn;
//general getters/setters
}
Second, define your custom serializer: (Please note that I used myDateObject.getDate().getClass().getName() to get the class name of date field.)
class DateSerializer extends StdSerializer<MyDateObject> {
public DateSerializer() {
this(null);
}
protected DateSerializer(Class<MyDateObject> t) {
super(t);
}
#Override
public void serialize(MyDateObject myDateObject, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
jsonGenerator.writeNumberField(myDateObject.getDate().getClass().getName(), myDateObject.getDate().getTime());
jsonGenerator.writeEndObject();
}
}
Finally, register the serializer with ObjectMapper for the MyDateObject class and perform the serialization:
MyDateObject myDateObject = new MyDateObject();
myDateObject.setDate(new Date());
Task task = new Task();
task.setTaskId(5081);
task.setTaskCreatedOn(myDateObject);
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(MyDateObject.class, new DateSerializer());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(simpleModule);
System.out.println(objectMapper.writeValueAsString(task));
The expected output is:
{"task-id":5081,"task-created-on":{"java.util.Date":1633402076254}}
Please refer to Jackson – Custom Serializer for more information.
It is possible to solve the issue with the use of a custom JsonSerializer and applying the JsonSerialize over the fields in the pojo you are interested like below :
public class Task {
#JsonProperty("task-id")
private int taskId;
#JsonProperty("task-created-on")
#JsonSerialize(using = ObjectSerializer.class)
Date taskCreatedOn;
}
The custom serializer will use the JsonGenerator.html#writeObjectField to serialize a generic object (Date or other java class) as propertyname : {"classname" : value} :
public class ObjectSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object t, JsonGenerator jg, SerializerProvider sp) throws IOException {
jg.writeStartObject();
jg.writeObjectField(t.getClass().getName(), t);
jg.writeEndObject();
}
}

Is there a common code to change the date format(timestamp) to Number format

I am using SpringBoot 2.2. date format is "validFrom": "2013-12-31T18:30:00.000+0000"
But I want in number format (like 1411471800000).
In my entity I included the below code snippet which worked in Number format.
#JsonProperty("updDate")
**#JsonFormat(shape = JsonFormat.Shape.NUMBER)**
private Date updDate;
To achieve that, I will have to do in all my entities.Is there a way where I can make one change and it will apply for all date formats.
Please advise
You can use custom Serializer for Date type which will used to serialize Date type.
public class DateSerializer extends StdSerializer<Date> {
private static final long serialVersionUID = -7880057299936791237L;
public JacksonLocalDateSerializer() {
this(null);
}
public JacksonLocalDateSerializer(Class<Date> type) {
super(type);
}
#Override
public void serialize(Date value, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeNumber(value.getTime());
}
}
and add it in object mapper so that Date type object always serialize using your custom serializer
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper configureObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(Date.class, new DateSerializer());
objectMapper.registerModule(javaTimeModule);
return objectMapper;
}
}

#JsonDeserializer in Mixin

Consider the following example:
I have a json string = {"timestamp":1504111920} which needs to be converted to CodeTimestamp class. The timestamp present in above json string is in epoch second.
CodeTimestamp class:
#Getter
#Setter
#NoArgsConstructor
class CodeTimestamp {
private Date timestamp;
}
By directly using fasterxml jackson mapper, I'll not be able to get the correct date since it assumes timestamp to be in epoch millisecond. So, I would need to write a custom deserializer.
However, I cannot edit/modify CodeTimestamp class. Is there any way to write JsonDeserializer in mixin?
I'm facing issues while deserializing. Following is the code:
public abstract class StreamRecordMixIn {
#JsonDeserialize(using = UnixTimestampDeserializer.class)
private Date approximateCreationDateTime;
}
public class UnixTimestampDeserializer extends JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
String unixTimestamp = parser.getText().trim();
return new Date(TimeUnit.SECONDS.toMillis(Long.valueOf(unixTimestamp)));
}
}
Code to initialize and use object mapper:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.addMixIn(CodeTimestamp.class, StreamRecordMixIn.class);
CodeTimestamp codeTimeStamp = objectMapper.readValue(payload, CodeTimestamp.class);
Error:
Caused by: java.lang.IllegalArgumentException: Class com.test.TestConverter$UnixTimestampDeserializer has no default (no arg) constructor
at com.fasterxml.jackson.databind.util.ClassUtil.createInstance(ClassUtil.java:378)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.deserializerInstance(DefaultDeserializationContext.java:218)
at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findDeserializerFromAnnotation(BasicDeserializerFactory.java:1735)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.constructSettableProperty(BeanDeserializerFactory.java:730)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.addBeanProps(BeanDeserializerFactory.java:507)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:229)
at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:142)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:403)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:352)
at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
... 23 more
The mistake here is custom deserializer not declared as static. So if I used it as mentioned below, it works.
public static class UnixTimestampDeserializer extends JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
String unixTimestamp = parser.getText().trim();
return new Date(TimeUnit.SECONDS.toMillis(Long.valueOf(unixTimestamp)));
}
}

Date from Spring to web as timestamp using Jakson

I have Match class and field Date start. My goal is get start as timestamp. I use Spring, AngularJs, and jackson as json converter.
Spring Controller:
#RequestMapping(value = "/web2/getMatch", method =RequestMethod.POST)
public #ResponseBody Match getPicksHistory() {
PickDAO pd = new PickDAO();
return pd.getMatch();
}
On AgularJS controler:
var res = $http.post(urlSer.url+"web2/getMatch");
res.success(function(data, status, headers, config) {
// now returns data.start = "Aug 8, 2015 7:00:00 PM"
// My goal is get as timestamp
});
I assume that by 'timestamp' you mean a numeric timestamp as opposed to a textual representation. You can use a custom ObjectMapper:
#Component
#Primary
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
}
}
I use jackson-databind:2.6.1 and JsonSerializer
#Component
public class JsonDateSerializer extends JsonSerializer<Date>{
#Override
public void serialize(Date date, JsonGenerator gen,
SerializerProvider serializers) throws IOException,
JsonProcessingException {
gen.writeNumber(date.getTime());
}
}

JSON serialization strategy for dates

The problem I am having is that I have some consumers that are Java and some that are browsers. My target browsers are IE7+ (json3 for IE7 only) & Chrome.
For a browser I wish to have the date deserialize to a Date JavaScript object (using the JSON.parse() method. For a Java consumer I wish to deserialize to a java.util.Date Java object.
Given that I can't change anything on the browser side. I have to do serialize the messages to something like this:
{ myDate: new Date(<EPOCH HERE>) }
Which of course will cause a problem for Java deserializer. However, I am hoping there is something I can do with Gson to make this work...amy ideas?
Or should I take a different strategy altogether?
I usually use the annotation #JsonSerialize and #JsonDeserialize to deal with this problem. I also use ISO8601 format as a standard for our REST API dates.
#JsonSerialize(using=JsonDateSerializer.class)
#JsonDeserialize(using=JsonDateDeserializer.class)
private Date expiryDate;
JsonDateSerializer class
#Component
public class JsonDateSerializer extends JsonSerializer<Date>
{
// ISO 8601
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
#Override
public void serialize(Date date, JsonGenerator gen, SerializerProvider provider)
throws IOException, JsonProcessingException
{
String formattedDate = dateFormat.format(date);
gen.writeString(formattedDate);
}
}
JsonDateDeserializer class
#Component
public class JsonDateDeserializer extends JsonDeserializer<Date>
{
// ISO 8601
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException
{
try
{
return dateFormat.parse(jsonParser.getText());
}
catch (ParseException e)
{
throw new JsonParseException("Could not parse date", jsonParser.getCurrentLocation(), e);
}
}
}

Categories

Resources