A Practical Guide to Formatting ZonedDateTime in Java

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 LetterMeaningExample
yyyyYear (4 digits)2025
MMMonth (01–12)07
ddDay of month (01–31)15
HHHour in 24h (00–23)14
mmMinute (00–59)30
ssSecond (00–59)45
SSSMilliseconds123
zTime-zone abbreviationEST
VVTime-zone ID (IANA)America/New_York
ZZone offset (+HHMM)-0500
XXXZone 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) and EEEE (day name) depend on the Locale. Always pass an explicit Locale to ofPattern() 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:00 does not unambiguously identify a timezone. Use zone IDs ([America/Chicago]) whenever you need round-trip fidelity.
  • Using SimpleDateFormat with ZonedDateTime — the legacy SimpleDateFormat does not understand java.time classes. Always use DateTimeFormatter.

See Also

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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.