ZonedDateTime.parse() reads a string such as 2026-03-14T10:15:30+01:00[Europe/Paris] and returns a ZonedDateTime, which is a date and a time of day together with a time zone and its UTC offset. Without a formatter, the method expects the ISO format with an offset and an optional zone ID in square brackets. For any other layout we pass a DateTimeFormatter whose pattern contains a zone or offset letter.
We parse strings to ZonedDateTime when a timestamp comes with its zone, for example a departure time from an airline API or a meeting time in a calendar export, and we need the exact moment as well as the local clock time.
The following example parses a full ISO string, a custom layout with a zone ID, and a string with no zone, which fails.
ZonedDateTime paris = ZonedDateTime.parse("2026-03-14T10:15:30+01:00[Europe/Paris]"); // 2026-03-14T10:15:30+01:00[Europe/Paris]
ZoneId zone = paris.getZone(); // Europe/Paris
DateTimeFormatter board = DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm VV");
ZonedDateTime tokyo = ZonedDateTime.parse("14/03/2026 10:15 Asia/Tokyo", board); // 2026-03-14T10:15+09:00[Asia/Tokyo]
ZonedDateTime noZone = ZonedDateTime.parse("2026-03-14T10:15:30"); // DateTimeParseException at index 19
Notice that the parser computes the offset +09:00 for Tokyo from the zone rules, because the input text has no offset. We start with the parts of the ISO format and move on to custom patterns, ambiguous zone names, strings without a zone, conflicts between offset and zone, and converting the result.
1. Parts of the ISO_ZONED_DATE_TIME Format
The one-argument ZonedDateTime.parse(CharSequence) uses DateTimeFormatter.ISO_ZONED_DATE_TIME. The text has four parts, and only the zone ID in brackets is optional.

The offset is the difference between the local clock time and UTC, such as +01:00, and the letter Z stands for an offset of zero. The zone ID, such as Europe/Paris, names a region whose offset changes with daylight saving time. Valid region names are listed in our ZoneId and ZoneOffset reference.
ZonedDateTime utc = ZonedDateTime.parse("2026-03-14T09:15:30Z"); // 2026-03-14T09:15:30Z
ZonedDateTime nanos = ZonedDateTime.parse("2026-03-14T10:15:30.123+01:00[Europe/Paris]"); // 2026-03-14T10:15:30.123+01:00[Europe/Paris]
ZonedDateTime lowerT = ZonedDateTime.parse("2026-03-14t10:15:30+01:00[Europe/Paris]"); // 2026-03-14T10:15:30+01:00[Europe/Paris]
ZonedDateTime lowerId = ZonedDateTime.parse("2026-03-14T10:15:30+01:00[europe/paris]"); // DateTimeParseException: unparsed text found at index 25
The letter T and the offset are case-insensitive, but the zone ID is case-sensitive, so europe/paris fails. A misspelled ID such as Europe/Pariss fails with the same message.
1.1. Offset-Only Strings and Region Zone IDs
A string without the bracket part is still valid, and the zone of the result is a ZoneOffset instead of a region. Both values describe the same moment, but they behave differently later.
ZonedDateTime fixed = ZonedDateTime.parse("2026-03-14T10:15:30+01:00");
ZonedDateTime region = ZonedDateTime.parse("2026-03-14T10:15:30+01:00[Europe/Paris]");
String fixedZone = fixed.getZone().toString(); // "+01:00"
boolean sameMoment = fixed.isEqual(region); // true
boolean sameValue = fixed.equals(region); // false
ZonedDateTime fixedJuly = fixed.plusMonths(4); // 2026-07-14T10:15:30+01:00
ZonedDateTime regionJuly = region.plusMonths(4); // 2026-07-14T10:15:30+02:00[Europe/Paris]
Only a region ID follows daylight saving time, so a value parsed from an offset-only string keeps +01:00 even in July. When the source sends only an offset and that is all we need, OffsetDateTime is the clearer type, as the ZonedDateTime vs OffsetDateTime comparison explains.
2. Custom Patterns With Zone and Offset Letters
For other layouts, we build the formatter with ofPattern() and pick the letter that matches the zone part of the text. The full letter list is in the DateTimeFormatter guide; these are the letters that carry a zone or an offset.
| Letter | Reads | Example text | Zone of the result |
|---|---|---|---|
| VV | Zone ID | Asia/Tokyo | Asia/Tokyo |
| z | Zone name or ID | PST, Europe/Paris | A region chosen by the JDK |
| XXX | ISO offset, Z for zero | -05:00, Z | ZoneOffset |
| xx / Z | Offset without colon | -0500 | ZoneOffset |
| O | Localized GMT offset | GMT+5:30 | ZoneOffset |
DateTimeFormatter withId = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm VV");
ZonedDateTime id = ZonedDateTime.parse("2026-03-14 10:15 America/New_York", withId); // 2026-03-14T10:15-04:00[America/New_York]
DateTimeFormatter withOffset = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss XXX");
ZonedDateTime offset = ZonedDateTime.parse("2026-03-14 10:15:30 -05:00", withOffset); // 2026-03-14T10:15:30-05:00
DateTimeFormatter compact = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss xx");
ZonedDateTime noColon = ZonedDateTime.parse("2026-03-14 10:15:30 -0500", compact); // 2026-03-14T10:15:30-05:00
DateTimeFormatter gmt = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss O");
ZonedDateTime india = ZonedDateTime.parse("2026-03-14 10:15:30 GMT+5:30", gmt); // 2026-03-14T10:15:30+05:30
2.1. Times With AM and PM
A 12-hour time needs hh (or h) together with a, and the formatter needs a locale whose AM/PM text matches the input. With HH, or with a lowercase am in the text, the parse fails.
DateTimeFormatter wrongLetters = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss a z");
ZonedDateTime broken = ZonedDateTime.parse("2026-03-27 10:15:30 am -05:00", wrongLetters); // DateTimeParseException at index 20
DateTimeFormatter twelveHour = DateTimeFormatter.ofPattern("uuuu-MM-dd hh:mm:ss a XXX", Locale.US);
ZonedDateTime morning = ZonedDateTime.parse("2026-03-27 10:15:30 AM -05:00", twelveHour); // 2026-03-27T10:15:30-05:00
If the source writes am in lowercase, we add parseCaseInsensitive() with a DateTimeFormatterBuilder. The 12-hour time format article covers the hour letters and the AM/PM text of other locales.
2.2. Ambiguous Zone Names Such as PST and IST
Short zone names are not unique, so the JDK maps each one to a region it picks. For example, PST becomes America/Los_Angeles, and the offset comes from that region on the parsed date, not from the letters in the text.
DateTimeFormatter named = DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm z", Locale.US);
ZonedDateTime pst = ZonedDateTime.parse("14/03/2026 10:15 PST", named); // 2026-03-14T10:15-07:00[America/Los_Angeles]
ZonedDateTime ist = ZonedDateTime.parse("14/03/2026 10:15 IST", named); // 2026-03-14T10:15Z[Africa/Abidjan]
Both results look wrong at first. On March 14, 2026, Los Angeles is already on daylight saving time (PDT, -07:00), even though the text says PST. And IST, which means India Standard Time to most developers, also stands for Irish and Israel Standard Time, so on JDK 25 with Locale.US it resolves to a GMT zone.
A travel app imports flight times from a partner feed in India, and the feed writes IST. We tell the parser which region we mean with appendZoneText() and a set of preferred zones.
DateTimeFormatter indiaFeed = new DateTimeFormatterBuilder()
.appendPattern("dd/MM/uuuu HH:mm ")
.appendZoneText(TextStyle.SHORT, Set.of(ZoneId.of("Asia/Kolkata")))
.toFormatter(Locale.US);
ZonedDateTime departure = ZonedDateTime.parse("14/03/2026 10:15 IST", indiaFeed); // 2026-03-14T10:15+05:30[Asia/Kolkata]
When we control the format, we write zone IDs with VV or numeric offsets with XXX, and never short names.
2.3. HTTP Dates in RFC 1123 Format
HTTP headers such as Last-Modified and Expires use the RFC 1123 format. The constant DateTimeFormatter.RFC_1123_DATE_TIME reads it, including the GMT suffix and numeric offsets.
ZonedDateTime header = ZonedDateTime.parse("Sat, 14 Mar 2026 09:15:30 GMT", DateTimeFormatter.RFC_1123_DATE_TIME); // 2026-03-14T09:15:30Z
ZonedDateTime offsetHeader = ZonedDateTime.parse("Sat, 14 Mar 2026 10:15:30 +0100", DateTimeFormatter.RFC_1123_DATE_TIME); // 2026-03-14T10:15:30+01:00
3. Strings Without a Time Zone
Many sources write only a local date and time, for example a spreadsheet of store sales where every row is in the store’s own time. The text alone does not say which moment it means, so ZonedDateTime.parse() cannot read it. We supply the zone ourselves in one of two ways.
- Parse the text as a LocalDateTime and attach the zone with atZone().
- Give the formatter a default zone with withZone(), so ZonedDateTime.parse() uses that zone when the text has none.
DateTimeFormatter local = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm");
ZoneId store = ZoneId.of("America/New_York");
ZonedDateTime sale = LocalDateTime.parse("2026-03-14 10:15", local).atZone(store); // 2026-03-14T10:15-04:00[America/New_York]
ZonedDateTime sale2 = ZonedDateTime.parse("2026-03-14 10:15", local.withZone(store)); // 2026-03-14T10:15-04:00[America/New_York]
We always name the zone with ZoneId.of() instead of ZoneId.systemDefault(), because the default zone of a cloud server is often UTC and differs from the zone of the data. The LocalDateTime to ZonedDateTime article covers atZone() in more depth.
A local time can fall into a daylight saving gap or overlap. In New York, 02:30 on March 8, 2026 does not exist, so atZone() moves it forward one hour. At 01:30 on November 1, 2026 the clock shows that time twice, and atZone() picks the earlier offset unless we ask for the later one.
ZoneId newYork = ZoneId.of("America/New_York");
ZonedDateTime gap = LocalDateTime.parse("2026-03-08T02:30").atZone(newYork); // 2026-03-08T03:30-04:00[America/New_York]
ZonedDateTime overlap = LocalDateTime.parse("2026-11-01T01:30").atZone(newYork); // 2026-11-01T01:30-04:00[America/New_York]
ZonedDateTime later = overlap.withLaterOffsetAtOverlap(); // 2026-11-01T01:30-05:00[America/New_York]
4. When the Offset and the Zone ID Disagree
A string can carry an offset that does not match its zone, for example +05:00[Europe/Paris] in winter, when Paris is at +01:00. In that case the parser keeps the instant that the date, time and offset describe, and recalculates the local time in the named zone.
ZonedDateTime mismatch = ZonedDateTime.parse("2026-03-14T10:15:30+05:00[Europe/Paris]"); // 2026-03-14T06:15:30+01:00[Europe/Paris]
ZonedDateTime inGap = ZonedDateTime.parse("2026-03-29T02:30:00+01:00[Europe/Paris]"); // 2026-03-29T03:30+02:00[Europe/Paris]
The method parse() does not throw for a mismatch, so the clock time in the result can differ from the clock time in the text. When the data comes from another system and the exact local time matters, we compare toLocalDateTime() with the text, or ask the sender to fix the offset.
5. Converting the Parsed Value
After parsing, we often show the time in another zone or store it as an instant. The method withZoneSameInstant() keeps the moment and changes the clock time, whereas withZoneSameLocal() keeps the clock time and changes the moment, which is rarely what we want.
ZonedDateTime flight = ZonedDateTime.parse("2026-03-14T23:30:00-04:00[America/New_York]");
ZonedDateTime arrivalView = flight.withZoneSameInstant(ZoneId.of("Asia/Kolkata")); // 2026-03-15T09:00+05:30[Asia/Kolkata]
Instant stored = flight.toInstant(); // 2026-03-15T03:30:00Z
LocalDate localDay = flight.toLocalDate(); // 2026-03-14
OffsetDateTime forJdbc = flight.toOffsetDateTime(); // 2026-03-14T23:30-04:00
We store the Instant or the OffsetDateTime in a database column of type TIMESTAMP WITH TIME ZONE, and keep the zone ID in its own column when the app must show the original local time later. To normalize every value to UTC on input, the parse a string to UTC article shows the steps, and the time zone conversion article covers more conversions.
6. ZonedDateTime Parsing FAQs
Questions about the error index, the zone letters and OffsetDateTime come up when parsing data from other systems.
6.1. Why does ZonedDateTime.parse() fail “at index 19”?
The text ends after the seconds, at index 19, where the parser expects an offset. The string has no zone, so we either parse it as a LocalDateTime and call atZone(), or give the formatter a zone with withZone(), as in section 3.
6.2. Should we use VV or z for zone IDs in a pattern?
We use VV. It reads and writes the zone ID, such as Europe/Paris, so the text round-trips without loss. The letter z also parses zone IDs, but it writes short names such as CET when formatting, and those names are ambiguous.
6.3. What is the difference between ZonedDateTime.parse() and OffsetDateTime.parse()?
OffsetDateTime.parse() reads a date, a time and an offset, and ignores daylight saving rules. ZonedDateTime.parse() also reads the optional zone ID and applies its rules in later calculations. For a value that crosses the wire or goes into a database, OffsetDateTime or Instant is enough.
6.4. How do we parse an epoch timestamp into a ZonedDateTime?
An epoch value is a number, not a date string, so we create an Instant and add the zone.
long epochSeconds = 1773530130L;
ZonedDateTime fromEpoch = Instant.ofEpochSecond(epochSeconds).atZone(ZoneId.of("Europe/Paris")); // 2026-03-15T00:15:30+01:00[Europe/Paris]
7. Conclusion
ZonedDateTime.parse(text) reads the ISO format with an offset and an optional zone ID in brackets. Only a region ID such as Europe/Paris follows daylight saving rules; an offset-only string gives a fixed ZoneOffset.
For custom layouts, the pattern letter must match the zone part of the text. We use VV for zone IDs and XXX for offsets, and we read short names with z only together with preferred zones. Text without a zone needs atZone() or withZone() with an explicit ZoneId. The ZonedDateTime class guide and the Java date and time tutorial cover the rest of the API.
8. References
- ZonedDateTime.parse() (Java SE 25 API)
- DateTimeFormatter.ISO_ZONED_DATE_TIME (Java SE 25 API)
- DateTimeFormatterBuilder.appendZoneText() (Java SE 25 API)
- RFC 1123
Happy Learning !!