Search code examples
javajsondatetimezoneobjectmapper

Date decrements by a day when converted to a JSON String - Java


We have a method that gets a date from an alpha-numeric String. Say that the String is Y04051000547GF the resultant date will be Tue May 04 00:00:00 IST 2010.

public static Date getDateFromGITID(String gitId) {
    
    Date date = null;
    try {

        String dateInGit = nid.substring(1, 7); //  six characters from the second character

        DateFormat format = new SimpleDateFormat("ddMMyy", Locale.ENGLISH);
        date = format.parse(dateInGit);
    } catch (ParseException e) {
        
    }
    
    return date;
}

Now, this date is assigned to an attribute of a class UserInfo, when we try to convert an object of this class to a JSON the date gets decremented by 1 day!

Object to JSON string

public static String asJsonString(final Object obj) {
    try {
        return new ObjectMapper().writeValueAsString(obj);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

User Info Bean

public class UserInfo {
    
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
    Date date;
}

This is main method

public static void main(String[] args) {
    
    UserInfo obj = new UserInfo();
    obj.date = getDateFromGITID("Y04051000547GF");
    
    System.out.println(obj.date);

    System.out.println(asJsonString(obj));
}

This was the console output

Tue May 04 00:00:00 IST 2010
{"dob":"2010-05-03"}

You can see that the date is 4th of May 2010, but in JSON it gets decremented by 1 day. We were able to figure out what was causing the issue, the date is printed in IST which has a difference of 5 hours and 30 minutes with UST. So we added that time before the JSON conversion and the converted time didn't have a change.

public static void main(String[] args) {
    
    UserInfo obj = new UserInfo();
    obj.dob = getDOBFromNID("Y04051000547GF");
    
    System.out.println(obj.dob);

    obj.dob.setHours(5);
    obj.dob.setMinutes(30);
    obj.dob.setSeconds(00);

    System.out.println(asJsonString(obj));
}

Output

Tue May 04 00:00:00 IST 2010
{"dob":"2010-05-04"}

Yes, we have used the deprecated method but it is just for testing. Even if we try to decrease the time by 1 second the issue will come.

Edit-1 (Found another fix by setting the time zone to the formatter)

We were able to fix this issue in another way when we set the time zone to GST (Java SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") gives timezone as IST) for the DateFormat inside the getDateFromGITID method.

format.setTimeZone(TimeZone.getTimeZone("GMT"));

So the questions are

  1. Why is it happening?
  2. Is the second way a better fix?
  3. Is there any other better fix?

Solution

  • You are using terribly flawed date-time classes that were years ago supplanted by the modern java.time classes defined in JSR 310.

    Specifically, I expect your problem is due to using java.util.Date#toString method to generate text. That method lies to you. A java.util.Date value represents a date and time as seen with an an offset of zero hours-minutes-seconds from the temporal meridian of UTC. The toString method applies the JVM’s current default time zone while generating its textual result.

    While well-intentioned, this anti-feature causes endless confusion and pain to Java developers. Avoid this class. Avoid all the legacy date-time classes!

    Do all your date-time work using the industry-leading java.time classes.

    String original = "Y04051000547GF" ;
    String input = original.substring( 1 , 7 ) ;
    DateTimeFormatter f = DateTimeFormatter.ofPattern( "ddMMuu" ) ;
    LocalDate ld = LocalDate.parse( input , f ) ;
    

    If you must interoperate with old code not yet updated to java.time, convert using new methods added to the old classes.

    Going from a date to a moment, a specific point on the timeline, requires a time of day, and requires a time zone. I will assume you want the first moment of that date as seen in UTC (an offset of zero).

    ZonedDateTime zdt = ld.atStartOfDay( ZoneOffset.UTC ) ;
    Instant instant = zdt.toInstant() ;
    java.util.Date d = java.util.Date.from( instant ) ;
    

    I must note how ill-advised is the use of a date-with-time-and-offset type like java.util.Date to represent a date-only value. If possible, consider reworking your codebase to use correct types. For a date-only value, use java.time.LocalDate class. LocalDate is supported by Java 8+, JDBC 4.2+, Hibernate, Jackson, etc.

    Or, as Commented by Ole V.V., perhaps you mean to get the first moment of the day as seen in a particular time zone.

    ZoneId z = ZoneId.of( "Asia/Kolkata" ) ;
    ZonedDateTime zdt = ld.atStartOfDay( z ) ;
    

    The best solution is to educate the publisher of your data to use standard ISO 8601 formats for data exchange of date-time values rather than invent their own.