ZonedDateTime is the most complete date-time class in the modern java.time API: it stores a date, a time, and a full timezone (e.g. America/New_York). Displaying it in a human-readable or machine-readable format requires the DateTimeFormatter class. In this tutorial you will learn every important formatting pattern, understand the difference between predefined ISO formatters and custom patterns, and see how to parse a string back into a ZonedDateTime — all with annotated code examples and sample output.
Quick Reference: Common Format Patterns
| Pattern Letter | Meaning | Example |
|---|---|---|
yyyy | Year (4 digits) | 2025 |
MM | Month (01–12) | 07 |
dd | Day of month (01–31) | 15 |
HH | Hour in 24h (00–23) | 14 |
mm | Minute (00–59) | 30 |
ss | Second (00–59) | 45 |
SSS | Milliseconds | 123 |
z | Time-zone abbreviation | EST |
VV | Time-zone ID (IANA) | America/New_York |
Z | Zone offset (+HHMM) | -0500 |
XXX | Zone offset with colon | -05:00 |
Using Predefined ISO Formatters
DateTimeFormatter ships with a set of constants for ISO 8601 — the standard format used in REST APIs, databases, and log files:
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class IsoFormatterDemo {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.of(2025, 7, 15, 14, 30, 45, 0,
ZoneId.of("America/New_York"));
// ISO_ZONED_DATE_TIME: includes offset and zone ID
System.out.println(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(zdt));
// ISO_OFFSET_DATE_TIME: includes offset but not zone name
System.out.println(DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(zdt));
// ISO_INSTANT: always UTC (useful for logging and APIs)
System.out.println(DateTimeFormatter.ISO_INSTANT.format(zdt));
}
}
2025-07-15T14:30:45-04:00[America/New_York]
2025-07-15T14:30:45-04:00
2025-07-15T18:30:45Z
Custom Format Patterns with ofPattern()
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class CustomFormatDemo {
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.of(2025, 12, 25, 9, 0, 0, 0,
ZoneId.of("Europe/London"));
// Friendly human-readable format
DateTimeFormatter friendly =
DateTimeFormatter.ofPattern("EEEE, MMMM d yyyy 'at' h:mm a z", Locale.ENGLISH);
System.out.println(friendly.format(zdt));
// Database-friendly format (no special chars)
DateTimeFormatter dbFormat =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
System.out.println(dbFormat.format(zdt));
// Short date + time with milliseconds
DateTimeFormatter shortFormat =
DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss.SSS");
System.out.println(shortFormat.format(zdt));
// IANA zone ID in the output
DateTimeFormatter withZoneId =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
System.out.println(withZoneId.format(zdt));
}
}
Thursday, December 25 2025 at 9:00 AM GMT
2025-12-25 09:00:00 +0000
25/12/2025 09:00:00.000
2025-12-25 09:00:00 Europe/London
Formatting ZonedDateTime to String (Shorthand)
ZonedDateTime.format(formatter) and formatter.format(zdt) produce the same result. The instance method on ZonedDateTime reads more naturally in a chain:
String result = ZonedDateTime.now(ZoneId.of("Asia/Kolkata"))
.format(DateTimeFormatter.ofPattern("dd-MMM-yyyy HH:mm z",
Locale.ENGLISH));
System.out.println(result); // e.g. 15-Jul-2025 19:30 IST
Parsing a String Back to ZonedDateTime
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class ParseDemo {
public static void main(String[] args) {
// Parse ISO 8601 with zone offset
ZonedDateTime zdt1 = ZonedDateTime.parse("2025-07-15T14:30:45-04:00[America/New_York]");
System.out.println("Parsed ISO : " + zdt1);
// Parse a custom format
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss z");
ZonedDateTime zdt2 = ZonedDateTime.parse("25/12/2025 09:00:00 GMT", fmt);
System.out.println("Parsed custom : " + zdt2);
// Parse and immediately reformat
String reformatted = ZonedDateTime.parse("2025-07-15T14:30:45-04:00[America/New_York]")
.format(DateTimeFormatter.ofPattern("MMMM d, yyyy h:mm a z", java.util.Locale.ENGLISH));
System.out.println("Reformatted : " + reformatted);
}
}
Parsed ISO : 2025-07-15T14:30:45-04:00[America/New_York]
Parsed custom : 2025-12-25T09:00Z[GMT]
Reformatted : July 15, 2025 2:30 PM EDT
Timezone Conversion then Format
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class TimezoneConvertDemo {
public static void main(String[] args) {
ZonedDateTime newYork = ZonedDateTime.of(2025, 7, 15, 14, 0, 0, 0,
ZoneId.of("America/New_York"));
// Convert to other zones and format each
DateTimeFormatter fmt =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z");
String[] zones = {"Europe/London", "Asia/Kolkata", "Asia/Tokyo", "Australia/Sydney"};
System.out.println("Original (New York) : " + newYork.format(fmt));
for (String zone : zones) {
ZonedDateTime converted = newYork.withZoneSameInstant(ZoneId.of(zone));
System.out.printf("%-25s: %s%n", zone, converted.format(fmt));
}
}
}
Original (New York) : 2025-07-15 14:00 EDT
Europe/London : 2025-07-15 19:00 BST
Asia/Kolkata : 2025-07-15 23:30 IST
Asia/Tokyo : 2025-07-16 04:00 JST
Australia/Sydney : 2025-07-16 05:00 AEST
Common Pitfalls
- Locale-sensitive patterns — patterns like
MMMM(month name) andEEEE(day name) depend on the Locale. Always pass an explicitLocaletoofPattern()in production code. - Missing zone in pattern — if your format string does not include a zone token (
z,Z,VV,XXX), timezone information is silently dropped from the output. - Parsing ambiguous offsets — a string with only an offset like
-05:00does not unambiguously identify a timezone. Use zone IDs ([America/Chicago]) whenever you need round-trip fidelity. - Using SimpleDateFormat with ZonedDateTime — the legacy
SimpleDateFormatdoes not understandjava.timeclasses. Always useDateTimeFormatter.
See Also
- Converting LocalDateTime to ZonedDateTime in Java
- Mastering Conversions Between LocalDate and ZonedDateTime
- Java Comparable vs Comparator
Conclusion
Formatting a ZonedDateTime is a two-step process: choose a DateTimeFormatter (predefined ISO constant or custom pattern via ofPattern()), then call format(). For REST APIs and logs, prefer DateTimeFormatter.ISO_INSTANT to ensure UTC output that is unambiguous across timezones. For user-facing strings, use ofPattern() with an explicit Locale. Always include a zone token in your pattern and use withZoneSameInstant() when you need to convert across timezones before formatting.