Dominik
Dominik

Reputation: 2906

Convert asp.net / MS proprietary json Dateformat to java8 LocalDateTime with jackson while deserializing json to object

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

Answers (2)

Jeff Sheets
Jeff Sheets

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

user7605325
user7605325

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

Related Questions