W3docs

Java Date Parsing

Parse strings into Java date-time objects with DateTimeFormatter and handle parsing exceptions.

Java Date Parsing

Parsing is the mirror of formatting. Same DateTimeFormatter, same pattern alphabet, same caveats — but reading a string instead of writing one. Every java.time type has a parse(...) factory; with a default pattern (ISO-8601) it takes one argument, and with a custom pattern it takes a formatter.

LocalDate    d  = LocalDate.parse("2025-11-04");                                       // ISO default
LocalTime    t  = LocalTime.parse("14:30:00");
LocalDateTime dt = LocalDateTime.parse("2025-11-04T14:30:00");                          // note the T
ZonedDateTime zdt = ZonedDateTime.parse("2025-11-04T14:30:00-05:00[America/New_York]");
Instant       i = Instant.parse("2025-11-04T19:30:00Z");                                // trailing Z mandatory

Each of these uses the type's default formatter — strict ISO-8601. For anything that isn't ISO-shaped, build a formatter and pass it.

Custom-pattern parsing

DateTimeFormatter f = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate d = LocalDate.parse("04/11/2025", f);                                         // British DMY

The pattern alphabet is the same as for formatting — dd MMM yyyy, HH:mm:ss, MM/dd/yyyy h:mm a. The matching rule is strict by default: every literal character in the pattern must appear in the input verbatim, and every field must have the right number of digits.

DateTimeFormatter dmy = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate.parse("4/11/2025", dmy);                                                       // FAILS: "dd" requires 2 digits
LocalDate.parse("04/11/2025", dmy);                                                      // OK

If your input is sometimes single-digit, use d/M/yyyy (which accepts 1-2 digits) or build the formatter with DateTimeFormatterBuilder and parseStrict(false). The single-letter form is the simpler fix.

Locale matters at the parse boundary

The same locale story as formatting:

DateTimeFormatter englishDay = DateTimeFormatter.ofPattern("EEEE, MMMM d", Locale.US);
DateTimeFormatter germanDay  = DateTimeFormatter.ofPattern("EEEE, d. MMMM",  Locale.GERMAN);

LocalDate.parse("Tuesday, November 4", englishDay.withYear(2025));   // year would need to be passed separately
LocalDate.parse("Dienstag, 4. November", germanDay);                  // German month names

Without the locale, Locale.getDefault() is used — and a server JVM's default locale is unpredictable. Always pass a locale when parsing month or day names from a string the user could type. The mirror of "always format with a locale" applies.

DateTimeParseException

A failed parse throws DateTimeParseException (a subclass of RuntimeException, so not declared on parse). The message tells you both the position and what was expected:

try {
  LocalDate.parse("2025-13-45");                              // month 13, day 45
} catch (DateTimeParseException e) {
  e.getParsedString();                                         // "2025-13-45"
  e.getErrorIndex();                                           // index where parsing gave up
  e.getMessage();                                              // human description
}

Two distinct failures land here:

  • Format mismatch. The string doesn't fit the pattern at all — "04 nov 2025" against "dd-MM-yyyy".
  • Value out of range. The string fits the pattern but a value is impossible — month 13, day 32.

Both throw the same class. Catch and report; never swallow silently.

Two-digit years and the MAX_VALUE trap

The yy (two-digit year) pattern has a documented but surprising default: it parses to the year closest to today within a 100-year window. LocalDate.parse("11/04/25", DateTimeFormatter.ofPattern("MM/dd/yy")) is 2025-11-04 in 2025 and 2125-11-04 in 2076. That's a feature for "near today" cases and a footgun for archive data.

The fix is to use yyyy whenever the input has four digits and to be explicit about the century window when it doesn't:

DateTimeFormatter f = new DateTimeFormatterBuilder()
    .appendPattern("MM/dd/")
    .appendValueReduced(ChronoField.YEAR, 2, 2, 1950)         // window starts at 1950
    .toFormatter();

If you're handing legacy data with yy, document the window in code. The default is "rolling pivot around today's year," which is not what you want for "all my data is from the 1980s."

Parsing without committing to a type

DateTimeFormatter.parse(String) returns a TemporalAccessor — the bottom of the type hierarchy. Useful when the input might be either a LocalDate or a LocalDateTime:

TemporalAccessor ta = DateTimeFormatter.ISO_DATE_TIME.parseBest(
    "2025-11-04T14:30:00",
    LocalDateTime::from,                                       // preferred
    LocalDate::from);                                          // fallback

parseBest(text, ...queries) tries each type's from method in order and returns the first one that succeeds. The result needs instanceof to use it for anything specific:

if (ta instanceof LocalDateTime ldt) ...
else if (ta instanceof LocalDate ld) ...

For most code, calling the specific type's parse(...) directly is simpler. parseBest is for the case where you accept multiple shapes (a CSV column that might be a date or a date-time).

Lenient parsing with the builder

DateTimeFormatterBuilder lets you assemble a formatter that's more forgiving:

DateTimeFormatter lenient = new DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM-dd[ HH:mm[:ss]]")                  // optional sections in []
    .parseLenient()                                            // accept missing leading zeros etc.
    .parseCaseInsensitive()                                    // ignore case on month/day names
    .toFormatter(Locale.US);

The bracket syntax [...] marks an optional section — that pattern parses both "2025-11-04" (no time) and "2025-11-04 14:30" (with time). Combined with parseLenient and parseCaseInsensitive, you can build a formatter that accepts a wider range of inputs without writing a custom parser.

This is overkill for code that controls both ends. Use the strict default unless you're reading user input or legacy data.

Instant.parse is strict

Instant.parse("2025-11-04T19:30:00Z") works. The trailing Z (UTC) is mandatory; any other offset (-05:00, +09:00) needs OffsetDateTime.parse or ZonedDateTime.parse first, then .toInstant():

Instant inst = OffsetDateTime.parse("2025-11-04T14:30:00-05:00").toInstant();

This is the canonical conversion when an external API hands you ISO-8601 strings with timezone offsets but no IANA zone.

A worked example: read a config, parse, react to bad input

The program below parses three date-shaped strings from a synthetic config: an ISO date, a custom-pattern date, and an ISO instant. It then demonstrates the lenient builder with an optional time section, the parseBest API for "either a date or a date-time," and the failure mode when the input doesn't match.

java— editable, runs on the server

What to take from the run:

  • The three ISO-default parsers each accepted exactly the standard form: yyyy-MM-dd for LocalDate, the T-separated form for LocalDateTime, and the Z-terminated form for Instant. No flexibility, no guessing — and that's the point. If your input fits, no formatter is needed; if it doesn't, building one is one line.
  • The lenient formatter accepted three different input shapes — date only, date with time, date with time and seconds — because the bracketed [...] section is optional. parseBest(text, LocalDateTime::from, LocalDate::from) picked the richest type each input supported. That's the right pattern when you accept user-entered or config dates with variable precision.
  • OffsetDateTime.parse(wire).toInstant() was the canonical bridge from "an ISO-8601 timestamp with an offset" to Instant. Instant.parse itself only accepts Z-suffixed UTC; anything else has to go through OffsetDateTime (or ZonedDateTime) first. The conversion is one line each way.
  • The German-locale parse only worked because the formatter was built with Locale.GERMAN. The default locale would have rejected "November" if the JVM was running in German (which expects Locale.GERMAN's "November" to be matched against the German names). Always pin the locale at the parse boundary — the formatter alone isn't enough; the locale controls month and day-name resolution.
  • The two failure blocks both threw DateTimeParseException with useful position information. getErrorIndex is where the parser gave up; getParsedString is the input as the parser saw it. Surface these in user-facing errors — "couldn't parse the date at character 5" is dramatically more helpful than "invalid date."

What's next

Formatting and parsing close out the string boundary. The next chapter, Java Temporal Adjusters, goes back to the value side and covers the built-in adjusters (firstDayOfMonth, nextOrSame(MONDAY), etc.) and how to write your own — useful whenever the date you want depends on the date you have ("first Tuesday after the 15th").

Practice

Practice

A web form lets users type a date in the format '04/11/2025' (day/month/year). Your code uses `LocalDate.parse(input, DateTimeFormatter.ofPattern('dd/MM/yyyy'))` and then asks 'is this date in the future?'. A user types '4/11/2025' (no leading zero on the day) and the parse throws. What's the smallest fix?