Converting asp.net/MS proprietary json Dateformat to java8 LocalDateTime with jackson when deserializing json to object
I am calling a webservice from a Spring Boot application using jackson-jsr-310 as a maven dependency to be able to use LocalDateTime
:
RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = this.createHeaders();
ResponseEntity<String> response;
response = restTemplate.exchange(uri,HttpMethod.GET,new HttpEntity<Object>(httpHeaders),String.class);
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
BusinessPartner test = mapper.readValue(response.getBody(), BusinessPartner.class);
My problem is on the last line, the code is throwing this error:
java.time.format.DateTimeParseException: Text '/ Date (591321600000) /' could not be parsed at index 0
The resulting JSON response.getBody()
looks like this:
{
"d":{
...
"Address":{...},
"FirstName":"asd",
"LastName":"asd",
"BirthDate":"\/Date(591321600000)\/",
}
}
And in my model class, I have the following member:
@JsonProperty("BirthDate")
private LocalDateTime birthDate;
So after doing a little searching here, I found out that this /Date(...)/
one seems to be a proprietary Dateformat that Jackson cannot deserialize into a default object.
Some questions advise to create a custom one SimpleDateFormat
and apply it to the opbject mapper which I was trying to do, but I think I missed the correct syntax formapper.setDateFormat(new SimpleDateFormat("..."));
I tried using for example mapper.setDateFormat(new SimpleDateFormat("/Date(S)/"));
or at the end even mapper.setDateFormat(new SimpleDateFormat("SSSSSSSSSSSS)"));
but that doesn't seem to work either, so I'm not in the know right now and hope some people here can help me.
change 1:
further exploration seems to be one way is to write a custom one DateDeSerializer
for jackson. So I tried this:
@Component
public class JsonDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private DateTimeFormatter formatter;
private JsonDateTimeDeserializer() {
this(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
public JsonDateTimeDeserializer(DateTimeFormatter formatter) {
this.formatter = formatter;
}
@Override
public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
if (parser.hasTokenId(JsonTokenId.ID_STRING)) {
String unixEpochString = parser.getText().trim();
unixEpochString = unixEpochString.replaceAll("[^\\d.]", "");
long unixTime = Long.valueOf(unixEpochString);
if (unixEpochString.length() == 0) {
return null;
}
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(unixTime), ZoneId.systemDefault());
localDateTime.format(formatter);
return localDateTime;
}
return null;
}
}
which actually returns almost what I want, annotating my fields in the model using
@JsonDeserialize(using = JsonDateTimeDeserializer.class)
but not exactly: This code returns a value LocalDateTime
: 1988-09-27T01:00
. But in the thirdparty system, the xml value is 1988-09-27T00:00:00
.
As obvious the ZoneId is here:
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(unixTime), ZoneId.systemDefault());
- problem other than wrong date format.
So can someone here please help me on how to switch to always use zeros for time
-part and get the date format correct? It would be great!
source to share
I am assuming the number 591321600000
is epoch milli (number of milliseconds from 1970-01-01T00:00:00Z
).
If so, I think I SimpleDateFormat
can't help you (at least I couldn't find a way to parse a date from milli epochs using this class). The template S
(as per the javadoc ) is used to format or parse the milliseconds time field (so its max value is 999) and won't work for your case.
The only way I can get it to work is to create a custom deserializer.
I first created this class:
public class SimpleDateTest {
@JsonProperty("BirthDate")
private LocalDateTime birthDate;
// getter and setter
}
Then I created my own deserializer and added it to the custom module:
// I'll explain all the details below
public class CustomDateDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String s = p.getText(); // s is "/Date(591321600000)/"
// assuming the format is always /Date(number)/
long millis = Long.parseLong(s.replaceAll("\\/Date\\((\\d+)\\)\\/", "$1"));
Instant instant = Instant.ofEpochMilli(millis); // 1988-09-27T00:00:00Z
// instant is in UTC (no timezone assigned to it)
// to get the local datetime, you must provide a timezone
// I'm just using system default, but you must use whatever timezone your system uses
return instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
}
}
public class CustomDateModule extends SimpleModule {
public CustomDateModule() {
addDeserializer(LocalDateTime.class, new CustomDateDeserializer());
}
}
Then I added this module to my cartographer and it worked:
// using reduced JSON with only the relevant field
String json = "{ \"BirthDate\": \"\\/Date(591321600000)\\/\" }";
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// add my custom module
mapper.registerModule(new CustomDateModule());
SimpleDateTest value = mapper.readValue(json, SimpleDateTest.class);
System.out.println(value.getBirthDate()); // 1988-09-26T21:00
Now a few comments about the deserializer method.
I first converted millis 591321600000
to Instant
(class representing instantaneous UTC). 591321600000
in milliseconds is equivalent 1988-09-27T00:00:00Z
.
But it is UTC date / time. To get the local date and time , you have to know which time zone you are in, because each time zone has a different date and time (everyone in the world is at the same instant, but their local date / time may be different , depending on where they are).
In my example, I just used ZoneId.systemDefault()
that gets the default timezone for my system. But if you don't want to depend on the default and want to use a specific timezone then use the method ZoneId.of("timezone name")
(you can get a list of all available timezone names with ZoneId.getAvailableZoneIds()
- this method returns all valid names accepted by the method ZoneId.of()
).
Since my default timezone America/Sao_Paulo
, this code sets birthDate
to 1988-09-26T21:00
.
If you don't want to convert to a specific time zone, you can use ZoneOffset.UTC
. So in the deserializer method, the last line would be:
return instant.atZone(ZoneOffset.UTC).toLocalDateTime();
Now the local date will be 1988-09-27T00:00
- since we are using UTC offset, there is no time zone change and the local date / time does not change.
PS: if you need to convert birthDate
back to MS custom format, you can write your own serializer and add to custom module. To convert LocalDateTime
to this format, you can do:
LocalDateTime birthDate = value.getBirthDate();
// you must know in what zone you are to convert it to epoch milli (using default as an example)
Instant instant = birthDate.atZone(ZoneId.systemDefault()).toInstant();
String msFormat = "/Date(" + instant.toEpochMilli() + ")/";
System.out.println(msFormat); // /Date(591321600000)/
Note that in order to convert a LocalDateTime
to, Instant
you need to know what time zone you are in. In this case, I recommend using the same timezone for serialization and deserialization (in your case, you can use ZoneOffset.UTC
instead ZoneId.systemDefault()
.