Convert LocalDate to ZonedDateTime and Back in Java

Convert LocalDate to ZonedDateTime in Java with atStartOfDay(zone) or atTime().atZone(), and back with toLocalDate(), including DST days and zone pitfalls.

LocalDate 2026-06-12 converted with atStartOfDay(zone) or atTime(15, 0).atZone(zone) into ZonedDateTime, and back with toLocalDate()

A LocalDate becomes a ZonedDateTime when we add a time of day and a time zone, which atStartOfDay(zone) does in one call for midnight and atTime(time).atZone(zone) does for any other time. The way back is toLocalDate(), which drops the time and the zone and keeps the year, month and day.

Typical cases are a booking date or a due date that has to become a real moment, for example to compare it with a timestamp or to store it as UTC.

The following example turns a hotel check-in date in Rome into the start of that day, into the 15:00 check-in time, and back into a date.

LocalDate checkIn = LocalDate.of(2026, 6, 12);
ZoneId rome = ZoneId.of("Europe/Rome");
ZonedDateTime dayStart = checkIn.atStartOfDay(rome);                         // 2026-06-12T00:00+02:00[Europe/Rome]
ZonedDateTime arrival = checkIn.atTime(15, 0).atZone(rome);                  // 2026-06-12T15:00+02:00[Europe/Rome]
ZonedDateTime inTokyo = arrival.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));   // 2026-06-12T22:00+09:00[Asia/Tokyo]
LocalDate back = arrival.toLocalDate();                                      // 2026-06-12

Notice that we pass the zone explicitly when we add it, so the JVM default zone is never used. The sections cover what each type holds, the midnight conversion and the day when midnight does not exist, adding a specific time, the way back with the date it returns, and a check-in example for guests abroad.

1. What LocalDate and ZonedDateTime Each Hold

A LocalDate holds only a year, a month and a day, such as 2026-06-12. It is not a moment, because June 12 starts at different instants in Rome and in Tokyo. A ZonedDateTime holds a date, a time, a zone ID and the offset the zone uses at that moment, so it is one exact point on the timeline.

LocalDate 2026-06-12 converted with atStartOfDay(zone) or atTime(15, 0).atZone(zone) into ZonedDateTime, and back with toLocalDate()
Going to ZonedDateTime adds a time and a zone; toLocalDate() drops both again

So the conversion to ZonedDateTime always needs two extra facts, the time of day and the zone, and the way back always loses them. The table shows which call adds what.

CallAddsReturns
date.atStartOfDay()Time 00:00LocalDateTime
date.atStartOfDay(zone)First valid time of the day and the zoneZonedDateTime
date.atTime(15, 0)The given timeLocalDateTime
dateTime.atZone(zone)The zone and its offsetZonedDateTime
ZonedDateTime.of(date, time, zone)Time and zone in one callZonedDateTime
zoned.toLocalDate()Nothing, drops time and zoneLocalDate

2. LocalDate to ZonedDateTime at the Start of the Day

The method atStartOfDay(ZoneId) returns the first moment of the date in the given zone. For almost every date and zone, that moment is 00:00, and the zone rules add the right offset for the season.

LocalDate checkIn = LocalDate.of(2026, 6, 12);
ZonedDateTime romeStart = checkIn.atStartOfDay(ZoneId.of("Europe/Rome"));        // 2026-06-12T00:00+02:00[Europe/Rome]
ZonedDateTime utcStart = checkIn.atStartOfDay(ZoneOffset.UTC);                   // 2026-06-12T00:00Z
ZonedDateTime winterStart = LocalDate.of(2026, 1, 12).atStartOfDay(ZoneId.of("Europe/Rome"));   // 2026-01-12T00:00+01:00[Europe/Rome]

The start of the day is not always midnight, because a daylight saving change can skip 00:00. Chile moves its clocks from 00:00 to 01:00 on September 6, 2026, so that day starts at 01:00 in Santiago. The method atStartOfDay(zone) knows this and returns the first valid time.

ZonedDateTime santiagoStart = LocalDate.of(2026, 9, 6).atStartOfDay(ZoneId.of("America/Santiago"));   // 2026-09-06T01:00-03:00[America/Santiago]

Code that builds midnight by hand, such as ZonedDateTime.of(date, LocalTime.MIDNIGHT, zone), is also moved to 01:00 by the gap rule, but only atStartOfDay(zone) is defined to return the earliest valid time of the day, so we prefer it. Day ranges and the end of a day are covered in the start and end of a day in Java.

3. Adding a Specific Time With atTime() and atZone()

When the date needs a time other than the start of the day, we add the time first and the zone second. The atTime() overloads of LocalDate return a LocalDateTime, and atZone() turns that into a ZonedDateTime.

  • The overload atTime(LocalTime time) takes a prepared LocalTime, such as a check-in time from configuration.
  • The overload atTime(int hour, int minute) and the ones with seconds and nanoseconds take plain numbers.
  • The factory ZonedDateTime.of(LocalDate, LocalTime, ZoneId) does both steps in one call.
LocalDate checkIn = LocalDate.of(2026, 6, 12);
LocalTime checkInTime = LocalTime.of(15, 0);
ZonedDateTime viaAtTime = checkIn.atTime(checkInTime).atZone(ZoneId.of("Europe/Rome"));       // 2026-06-12T15:00+02:00[Europe/Rome]
ZonedDateTime viaOf = ZonedDateTime.of(checkIn, checkInTime, ZoneId.of("Europe/Rome"));       // 2026-06-12T15:00+02:00[Europe/Rome]
ZonedDateTime withSeconds = checkIn.atTime(15, 0, 30).atZone(ZoneId.of("Europe/Rome"));      // 2026-06-12T15:00:30+02:00[Europe/Rome]

The atZone() step attaches the zone to the local value as it is, so 15:00 stays 15:00. If the time falls into a daylight saving gap, Java moves it forward, and the rules for gaps and overlaps are in LocalDateTime to ZonedDateTime.

4. ZonedDateTime to LocalDate With toLocalDate()

The method toLocalDate() returns the date part of the ZonedDateTime as it reads in its own zone. It does not convert anything, so the result depends on the zone the value is in.

ZonedDateTime booked = ZonedDateTime.of(2026, 6, 11, 23, 30, 0, 0, ZoneId.of("America/New_York"));
LocalDate inNewYork = booked.toLocalDate();                                                     // 2026-06-11
LocalDate inRome = booked.withZoneSameInstant(ZoneId.of("Europe/Rome")).toLocalDate();          // 2026-06-12
LocalDate fromInstant = LocalDate.ofInstant(booked.toInstant(), ZoneId.of("Europe/Rome"));      // 2026-06-12

A booking made at 23:30 in New York is already June 12 in Rome. When the date must match a particular zone, we convert with withZoneSameInstant() first and call toLocalDate() after that. The method LocalDate.ofInstant(), added in Java 9, does both steps for an Instant.

5. Showing Check-In Times to Guests Abroad

A hotel booking site stores the check-in date as a LocalDate and each hotel’s check-in time and zone in its profile. A guest from Tokyo wants to see when the room is ready on their own clock, and the cancellation deadline is 18:00 hotel time on the day before arrival.

record Hotel(ZoneId zone, LocalTime checkInTime) {}
static ZonedDateTime checkInFor(Hotel hotel, LocalDate date, ZoneId guestZone) {
    return date.atTime(hotel.checkInTime()).atZone(hotel.zone()).withZoneSameInstant(guestZone);
}
Hotel romeHotel = new Hotel(ZoneId.of("Europe/Rome"), LocalTime.of(15, 0));
LocalDate stay = LocalDate.of(2026, 6, 12);
ZonedDateTime forTokyoGuest = checkInFor(romeHotel, stay, ZoneId.of("Asia/Tokyo"));              // 2026-06-12T22:00+09:00[Asia/Tokyo]
ZonedDateTime cancelBy = stay.minusDays(1).atTime(18, 0).atZone(romeHotel.zone());               // 2026-06-11T18:00+02:00[Europe/Rome]
boolean canCancel = Instant.parse("2026-06-11T15:30:00Z").isBefore(cancelBy.toInstant());         // true

Every rule is expressed in the hotel’s zone, and the guest’s zone is used only for display. The last line compares instants, so the check gives the same answer on a server in any zone.

6. LocalDate and ZonedDateTime FAQs

Developers who turn booking dates into moments ask about the UTC case, instants and the JVM default zone.

6.1. What Is the Difference Between atStartOfDay(zone) and atStartOfDay().atZone(zone)?

For almost every date, none. Both return 00:00 in the zone. The difference is in the definition. The method atStartOfDay(zone) returns the earliest valid time of the day, whereas atZone() on the LocalDateTime 00:00 shifts a time in a gap forward by the length of the gap. We use atStartOfDay(zone) because it says what we mean.

6.2. How Do We Convert a LocalDate to a ZonedDateTime in UTC?

We pass ZoneOffset.UTC as the zone, as in date.atStartOfDay(ZoneOffset.UTC). The result prints with a Z and no zone ID in brackets, because ZoneOffset is a fixed offset.

6.3. How Do We Convert a LocalDate to an Instant?

We go through ZonedDateTime and call toInstant(), because the instant depends on the zone.

Instant romeMidnight = LocalDate.of(2026, 6, 12).atStartOfDay(ZoneId.of("Europe/Rome")).toInstant();   // 2026-06-11T22:00:00Z

6.4. Should We Use ZoneId.systemDefault() for the Conversion?

Only for code that runs on a user’s own machine, such as a desktop app. A server app gets the zone of the machine or container it runs on, which is often UTC and rarely the zone of the user, so we pass the zone of the user, the hotel or the business rule. The JVM default time zone article explains where the default comes from.

7. Conclusion

To turn a LocalDate into a ZonedDateTime, we use atStartOfDay(zone) for the first moment of the day, and atTime(time).atZone(zone) or ZonedDateTime.of(date, time, zone) for any other time. The start of the day can be 01:00 on a daylight saving day, which atStartOfDay(zone) handles for us.

The way back is toLocalDate(), which reads the date in the value’s own zone. When the date must belong to another zone, we convert with withZoneSameInstant() first. More conversions between the date types are listed on the Java date and time hub page.

8. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I don’t agree with last Topic which convert ZonedDateTime to LocalDate. I have an issue about converting this, if my ZonedDateTime value is 2024-04-25 07:00:00.000 +0700. The returned LocalDate value will be 2024-04-24 (with time 17:00:00)

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.