Formatting and Parsing Dates

Turn dates into text and back with DateTimeFormatter, choose the right pattern letters and locale, and reject bad input cleanly.

Syntaxstatic final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("d MMM uuuu", Locale.UK); String text = DAY.format(date); LocalDate back = LocalDate.parse(text, DAY);

DateTimeFormatter converts between java.time values and text. Unlike the old SimpleDateFormat, it is immutable and thread-safe, so the usual pattern is a static final constant shared by every request.

Built-in formats first

Each type's toString() and parse() already use ISO-8601: LocalDate.parse("2026-10-13") needs no formatter. For machine-to-machine text, stick to those and the constants such as ISO_LOCAL_DATE, ISO_OFFSET_DATE_TIME and ISO_INSTANT. HTTP headers like Date and Last-Modified use RFC_1123_DATE_TIME, which prints Tue, 13 Oct 2026 09:30:00 GMT. It writes days below 10 without a leading zero (Sat, 3 Oct 2026), while HTTP's preferred format wants 03; for response headers, a ofPattern("EEE, dd MMM uuuu HH:mm:ss 'GMT'", Locale.ENGLISH) formatter with withZone(ZoneOffset.UTC) is the safe choice.

Custom patterns

DateTimeFormatter.ofPattern("d MMMM uuuu", Locale.UK) builds a formatter for human-facing text. The letters that cause most bugs:

LetterMeansCommon mix-up
MMmonthmm is minute
HHhour 0-23hh is hour 1-12 and needs a
uuuuyearYYYY is week-based year
ddday of monthDD is day of year

YYYY is the worst of these. It matches the calendar year for most of the year and then prints the next year for the last few days of December, so 29 December 2025 formats as 2026-12-29. It passes every test written in June. Week rules also come from the locale, so the exact days affected differ between Locale.US and Locale.UK.

Locale

Month and day names come from a Locale. Without one, ofPattern uses the JVM default, which differs between a developer's laptop and a container. Always pass the locale for names, and pass the user's locale when rendering for that user.

Parsing untrusted input

Parsing throws DateTimeParseException, a RuntimeException, so catch it at the API boundary and turn it into a 400 response that names the field and the expected format. getErrorIndex() says where the text went wrong.

Watch the resolver style. Formatters built with ofPattern use SMART resolution, which quietly turns 2026-02-30 into 28 February. Add .withResolverStyle(ResolverStyle.STRICT) to reject it, and use uuuu rather than yyyy with STRICT: yyyy is year-of-era, and strict parsing then also wants an era field, so even valid dates fail. The ISO constants are already strict.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Write the Last-Modified header of a file download with a fixed two-digit-day pattern in GMT, which HTTP clients and caches parse reliably.
  • Parse a dd/MM/uuuu date from a legacy CSV import with a strict formatter and report the row and index of bad values.
  • Render order dates in confirmation emails in the customer's own locale and zone from one shared formatter constant.

More examples

Turning bad input into a 400

With STRICT resolution 30/02/2026 is rejected instead of silently becoming 28 February. Its error index is 0 because each field parsed and only the combined date was invalid; the other inputs fail where the text stops matching.

Example · java
static final DateTimeFormatter UK_DATE =
        DateTimeFormatter.ofPattern("dd/MM/uuuu").withResolverStyle(ResolverStyle.STRICT);

String parseField(String field, String raw) {
    try {
        return "200 " + LocalDate.parse(raw, UK_DATE);
    } catch (DateTimeParseException e) {
        return "400 " + field + " must be dd/MM/yyyy (problem at index " + e.getErrorIndex() + ")";
    }
}

void main() {
    IO.println(parseField("startDate", "13/10/2026"));
    IO.println(parseField("startDate", "2026-10-13"));
    IO.println(parseField("startDate", "30/02/2026"));
    IO.println(parseField("startDate", "13/10/26"));
}
// Output:
// 200 2026-10-13
// 400 startDate must be dd/MM/yyyy (problem at index 2)
// 400 startDate must be dd/MM/yyyy (problem at index 0)
// 400 startDate must be dd/MM/yyyy (problem at index 6)

SMART versus STRICT resolution

SMART clamps an impossible day to the end of the month, which can book a customer on the wrong date. With STRICT, use uuuu: yyyy is year-of-era, which strict mode cannot turn into a date without an era.

Example · java
void main() {
    DateTimeFormatter smart = DateTimeFormatter.ofPattern("uuuu-MM-dd");
    DateTimeFormatter strict = smart.withResolverStyle(ResolverStyle.STRICT);
    DateTimeFormatter strictYyyy = DateTimeFormatter.ofPattern("yyyy-MM-dd")
            .withResolverStyle(ResolverStyle.STRICT);

    IO.println("smart:  " + LocalDate.parse("2026-02-30", smart));
    try {
        LocalDate.parse("2026-02-30", strict);
    } catch (DateTimeParseException e) {
        IO.println("strict: " + e.getMessage());
    }
    try {
        LocalDate.parse("2026-02-28", strictYyyy);
    } catch (DateTimeParseException e) {
        IO.println("yyyy:   rejected even a valid date");
    }
}
// Output:
// smart:  2026-02-28
// strict: Text '2026-02-30' could not be parsed: Invalid date 'FEBRUARY 30'
// yyyy:   rejected even a valid date

Rendering for the user's locale and zone

The stored value stays an Instant; only the last step picks a zone and a locale, and the zone can move the date as it does for Tokyo. withLocale returns a new formatter, so one shared constant serves every user.

Example · java
static final DateTimeFormatter PATTERN = DateTimeFormatter.ofPattern("d MMM uuuu, HH:mm");

String render(Instant at, ZoneId zone, Locale locale) {
    return at.atZone(zone).format(PATTERN.withLocale(locale)) + " " + zone.getId();
}

void main() {
    Instant shipped = Instant.parse("2026-10-13T21:45:00Z");
    IO.println(render(shipped, ZoneId.of("Europe/London"), Locale.UK));
    IO.println(render(shipped, ZoneId.of("America/New_York"), Locale.US));
    IO.println(render(shipped, ZoneId.of("Asia/Tokyo"), Locale.UK));
}
// Output:
// 13 Oct 2026, 22:45 Europe/London
// 13 Oct 2026, 17:45 America/New_York
// 14 Oct 2026, 06:45 Asia/Tokyo

Discussion

  • Be the first to comment on this lesson.