Reputation: 2906
I call a webservice from a Spring Boot App, using jackson-jsr-310 as maven dependency for being able to make use of 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 in the last line, the code produces this error:
java.time.format.DateTimeParseException: Text '/Date(591321600000)/' could not be parsed at index 0
The resulting JSON in 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 a bit of searching here I found out that this /Date(...)/
seems to be a Microsoft-proprietary Dateformat, which Jackson cannot deserialize into an object per default.
Some questions advise to create a custom SimpleDateFormat
and apply it to the opbject mapper, which I tried to do, but then I think I miss the right syntax for mapper.setDateFormat(new SimpleDateFormat("..."));
I tried with e.g. mapper.setDateFormat(new SimpleDateFormat("/Date(S)/"));
or at the end even mapper.setDateFormat(new SimpleDateFormat("SSSSSSSSSSSS)"));
but it seems this does not work, too, so I am out of ideas for now and hope some people here could help me out.
edit 1:
further investigated, it seems one way to go is to write a custom 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 nearly what I want, annotating my fields in the model using
@JsonDeserialize(using = JsonDateTimeDeserializer.class)
but not exactly:
This code returns a LocalDateTime
of value: 1988-09-27T01:00
.
But in the thirdparty system, the xmlvalue is 1988-09-27T00:00:00
.
As it is obvious, the ZoneId here:
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(unixTime), ZoneId.systemDefault());
is the Problem, apart from a wrong dateformat.
So could someone here please help me out in how to switch to always use zeros for the time
-part and to get my dateformat right? Would be great!
Upvotes: 4
Views: 425
Reputation: 1209
Here's some Groovy code I wrote that also handles the timezone offset: https://gist.github.com/jeffsheets/938733963c03208afd74927fb6130884
class JsonDotNetLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
@Override
LocalDateTime deserialize(JsonParser parser, DeserializationContext ctxt) {
convertDotNetDateToJava(parser.text.trim())
}
/**
* Returns a Java LocalDateTime when given a .Net Date String
* /Date(1535491858840-0500)/
*/
static LocalDateTime convertDotNetDateToJava(String dotNetDate) {
// Strip the prefix and suffix to just 1535491858840-0500
String epochAndOffset = dotNetDate[6..-3]
// 1535491858840
String epoch = epochAndOffset[0..-6]
// -0500 Note, keep the negative/positive indicator
String offset = epochAndOffset[-5..-1]
ZoneId zoneId = ZoneId.of("UTC${offset}")
LocalDateTime.ofInstant(Instant.ofEpochMilli(epoch.toLong()), zoneId)
}
}
Upvotes: 1
Reputation:
I'm assuming that the number 591321600000
is the epoch milli (number of milliseconds from 1970-01-01T00:00:00Z
).
If that's the case, I think that SimpleDateFormat
can't help you (at least I couldn't find a way to parse a date from the epoch milli using this class). The pattern S
(according to javadoc) is used to format or parse the milliseconds field of a time (so its maximum value is 999) and won't work for your case.
The only way I could make it work is creating a custom deserializer.
First, I created this class:
public class SimpleDateTest {
@JsonProperty("BirthDate")
private LocalDateTime birthDate;
// getter and setter
}
Then I created the custom deserializer and added it to a 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's 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 mapper 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 some comments about the deserializer method.
First I converted the millis 591321600000
to an Instant
(a class that represents a UTC instant). 591321600000
in millis is equivalent to 1988-09-27T00:00:00Z
.
But that's the UTC date/time. To get the local date and time, you must know in what timezone you are, because in every timezone it's a different date and time (everybody in the world are at the same instant, but their local date/time might be different, depending on where they are).
In my example, I just used ZoneId.systemDefault()
, which gets the default timezone of my system. But if you don't want to depend on the default and want to use a specific timezone, use the ZoneId.of("timezone name")
method (you can get the list of all available timezones names with ZoneId.getAvailableZoneIds()
- this method returns all valid names accepted by the ZoneId.of()
method).
As my default timezone is America/Sao_Paulo
, this code sets the birthDate
to 1988-09-26T21:00
.
If you don't want to convert to a specific timezone, you can use the ZoneOffset.UTC
. So, in the deserializer method, the last line will be:
return instant.atZone(ZoneOffset.UTC).toLocalDateTime();
Now the local date will be 1988-09-27T00:00
- as we're using UTC offset, there's no timezone conversion and the local date/time is not changed.
PS: if you need to convert the birthDate
back to MS's custom format, you can write a custom serializer and add to the custom module as well. To convert a LocalDateTime
to that 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, to convert a LocalDateTime
to Instant
, you must know in what timezone you are. In this case, I recommend to use the same timezone for serializing and deserializing (in your case, you can use ZoneOffset.UTC
instead of ZoneId.systemDefault()
.
Upvotes: 4