Directly converting between local and timezone-aware types requires an explicit ZoneId or ZoneOffset to bridge the gap. If this context is omitted, the operation fails with a DateTimeException at runtime, as the API refuses to resolve the ambiguity by assuming a default timezone.

Why is this an issue?

The java.time API distinguishes between local and absolute time. Classes such as LocalDate, LocalTime and LocalDateTime represent "wall-clock" time: human-readable date and time components that exist independent of any specific location. In contrast, the Instant class represents a single, unambiguous point on the UTC timeline, and ZonedDateTime and OffsetDateTime correspond to moments in time associated with a specific timezone or UTC offset.

Because local classes lack a geographical context, they cannot be mapped to the global timeline without a time zone (ZoneId) or UTC offset (ZoneOffset). Consequently, attempting a direct conversion between these types without providing the missing temporal context will result in a DateTimeException, as the Java runtime cannot make arbitrary assumptions about missing timezone information.

Similarly, an Instant cannot be transformed into a ZonedDateTime or OffsetDateTime without explicitly specifying the target zone, as a single moment in time translates to different wall-clock values across the globe.

What is the potential impact?

This issue causes runtime failures with DateTimeException, leading to application crashes. It is particularly problematic in production, where timezone bugs may only surface in different geographical regions.

How to fix it

Provide explicit timezone information (ZoneId or ZoneOffset) when converting between one of the following local date/time types and Instant, ZonedDateTime, OffsetDateTime or OffsetTime:

When converting an Instant to a ZonedDateTime, OffsetDateTime or OffsetTime, explicit timezone or offset information should also be specified.

Code examples

Noncompliant code example

public LocalDateTime fromInstant(Instant instant) {
    return LocalDateTime.from(instant); // Noncompliant - throws a DateTimeException
}
LocalDate date = LocalDate.of(2023, 12, 25);
Instant instant = Instant.from(date); // Noncompliant - throws a DateTimeException
public Year fromInstant(Instant instant) {
    return Year.from(instant); // Noncompliant - throws a DateTimeException
}
public ZonedDateTime fromInstant(Instant instant) {
    return ZonedDateTime.from(instant); // Noncompliant - throws a DateTimeException
}
public ZonedDateTime fromLocalDateTime(LocalDateTime localDateTime) {
    return ZonedDateTime.from(localDateTime); // Noncompliant - throws a DateTimeException
}

Compliant solution

public LocalDateTime fromInstant(Instant instant, ZoneId zone) {
    return LocalDateTime.ofInstant(instant, zone); // Compliant
}
LocalDate date = LocalDate.of(2023, 12, 25);

// Option 1: use a ZoneId
Instant instant1 = date.atStartOfDay(ZoneId.systemDefault()).toInstant(); // Compliant

// Option 2: use a ZoneOffset
Instant instant2 = date.atStartOfDay().toInstant(ZoneOffset.UTC); // Compliant
public Year fromInstant(Instant instant, ZoneId zone) {
    return Year.from(instant.atZone(zone)); // Compliant
}
public ZonedDateTime fromInstant(Instant instant, ZoneId zone) {
    return ZonedDateTime.ofInstant(instant, zone); // Compliant
}
public ZonedDateTime fromLocalDateTime(LocalDateTime localDateTime, ZoneId zone) {
    return ZonedDateTime.from(localDateTime.atZone(zone)); // Compliant
}

Resources

Documentation

Articles & blog posts