Back to Blog
Java

Java DateTimeFormatter: Formatting and Parsing Dates

java datetimeformatter: Learn how to use Java's DateTimeFormatter to format and parse dates, including pattern letters, builder usage, thread safety, and common pitfalls.

DateTimeFormatterJava time APIdate formattingdate parsingISO 8601thread safety
A Java DateTimeFormatter converting a date object to a formatted string and back.

java datetimeformatter requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Java's DateTimeFormatter is the standard API for converting between date-time objects and their string representations. It handles both formatting (object to string) and parsing (string to object) with a rich set of pattern letters and predefined ISO formats. Because DateTimeFormatter instances are immutable and thread-safe, they can be safely shared across threads, making them a reliable choice for production code.

Creating a DateTimeFormatter

The simplest way to obtain a DateTimeFormatter is through DateTimeFormatter.ofPattern(), which accepts a pattern string composed of specific letters. For example, DateTimeFormatter.ofPattern("yyyy-MM-dd") formats a date as 2025-03-14. You can also use predefined constants like DateTimeFormatter.ISO_LOCAL_DATE or DateTimeFormatter.ISO_OFFSET_DATE_TIME when you need standard ISO 8601 representations.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate date = LocalDate.of(2025, 3, 14); String formatted = date.format(formatter); // "2025-03-14"

The pattern letters are case-sensitive and each letter represents a specific field. For instance, y is year, M is month, d is day of month, H is hour of day, m is minute, s is second. Repeating a letter changes the width: yyyy gives a four-digit year, MM gives a two-digit month, and so on.

Formatting Date-Time Objects

DateTimeFormatter works with any TemporalAccessor, but you'll most often use it with LocalDate, LocalDateTime, ZonedDateTime, or OffsetDateTime. The format method is called on the formatter, passing the date-time object as an argument.

LocalDateTime now = LocalDateTime.of(2025, 3, 14, 15, 30, 45); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"); String formatted = now.format(formatter); // "14/03/2025 15:30:45"

For ZonedDateTime, you can include the zone offset and zone ID in the pattern. Letters like z represent the zone name, Z represents the offset, and XXX produces an ISO-style offset like +01:00.

ZonedDateTime zoned = ZonedDateTime.of(2025, 3, 14, 15, 30, 45, 0, ZoneId.of("Europe/Paris")); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); String formatted = zoned.format(formatter); // "2025-03-14 15:30:45 CET"

If the pattern does not include a zone or offset, the formatter uses the system default time zone for ZonedDateTime. To control this explicitly, use withZone() on the formatter.

Parsing Date-Time Strings

The parse method converts a string into a date-time object. The target type is determined by the TemporalQuery you pass, usually LocalDate::from, LocalDateTime::from, or ZonedDateTime::from. If the string does not match the pattern, a DateTimeParseException is thrown.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); LocalDate date = LocalDate.parse("2025-03-14", formatter);

For strings that include a time and offset, you can parse into OffsetDateTime or ZonedDateTime.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss XXX"); OffsetDateTime odt = OffsetDateTime.parse("2025-03-14 15:30:45 +01:00", formatter);

When the pattern is missing a field that the target type requires, parsing fails. For example, parsing LocalDate from a string that contains only a year and month will throw an exception because the day is missing. Use DateTimeFormatterBuilder with parseDefaulting to supply default values for missing fields.

Pattern Letters Reference

The following table lists the most commonly used pattern letters. Each letter is case-sensitive and its meaning depends on the number of repetitions.

LetterFieldExample (repetition)Output
yYearyyyy2025
MMonthMM03
dDay of monthdd14
HHour of day (0-23)HH15
mMinutemm30
sSecondss45
SFraction of secondSSS123
zZone namezCET
ZZone offsetZ+0100
XXXISO zone offsetXXX+01:00

Note that M and m are different: uppercase M is month, lowercase m is minute. Similarly, H is hour in 24-hour format, while h is hour in 12-hour format (used with a for AM/PM).

Using DateTimeFormatterBuilder for Complex Patterns

When a pattern is too complex or requires optional sections, DateTimeFormatterBuilder gives you fine-grained control. For example, you can make a time zone offset optional, or parse a date that may or may not include a time.

DateTimeFormatter formatter = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM-dd") .optionalStart() .appendLiteral(' ') .appendPattern("HH:mm") .optionalEnd() .toFormatter(); LocalDateTime dateTime = LocalDateTime.parse("2025-03-14 15:30", formatter); LocalDate date = LocalDate.parse("2025-03-14", formatter);

The builder also allows you to add default values for missing fields with parseDefaulting, which is useful when parsing partial dates.

DateTimeFormatter formatter = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM") .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) .toFormatter(); LocalDate firstOfMonth = LocalDate.parse("2025-03", formatter); // 2025-03-01

Thread Safety and Reuse

DateTimeFormatter is immutable and thread-safe. Once created, you can safely share it across multiple threads without synchronization. This makes it a good candidate for a static final constant in a utility class.

public class DateUtils { public static final DateTimeFormatter ISO_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); }

Because the formatter holds no mutable state, reusing the same instance avoids the overhead of creating a new formatter for each call. This is especially important in high-throughput code where formatting or parsing happens frequently.

Handling Locale and Time Zone

By default, DateTimeFormatter uses the system locale for textual fields like month names and day-of-week names. To control this, use withLocale().

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.FRANCE); String formatted = LocalDate.of(2025, 3, 14).format(formatter); // "14 mars 2025"

Similarly, withZone() sets the time zone used when formatting a ZonedDateTime or when parsing a string that lacks a zone. This is useful when you want to display times in a specific zone regardless of the source object's zone.

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z") .withZone(ZoneId.of("America/New_York")); ZonedDateTime zoned = ZonedDateTime.of(2025, 3, 14, 15, 30, 45, 0, ZoneId.of("Europe/Paris")); String formatted = zoned.format(formatter); // "2025-03-14 09:30 EST"

Note that withZone() does not change the time zone of the date-time object; it only affects how the formatter interprets or displays the zone.

Common Pitfalls and Edge Cases

One frequent mistake is using the wrong case for pattern letters, such as using M for minutes instead of m. This leads to a DateTimeParseException or incorrect output. Always verify the pattern against the DateTimeFormatter documentation.

Another issue is that DateTimeFormatter is strict by default. If the input string has extra characters or does not match the pattern exactly, parsing fails. For lenient parsing, you can use ResolverStyle.LENIENT via withResolverStyle(), but this is rarely recommended because it can silently accept invalid dates like February 30th.

When parsing strings that include a time zone offset, remember that LocalDateTime cannot store an offset. Use OffsetDateTime or ZonedDateTime instead. Similarly, if you parse a string with a zone but target LocalDateTime, the zone is ignored, which may hide data loss.

Performance Considerations

Creating a new DateTimeFormatter for every format or parse operation is wasteful. The pattern parsing and internal field setup happen each time, which adds unnecessary overhead. Reusing a single DateTimeFormatter instance is the simplest way to avoid this cost.

For very high-throughput scenarios, consider using predefined ISO formatters like ISO_LOCAL_DATE or ISO_INSTANT because they are pre-compiled and highly optimized. Custom patterns are still efficient, but they incur a one-time setup cost that is best amortized over many uses.

Also be aware that DateTimeFormatter is not null-safe: passing null to format or parse will throw a NullPointerException. Always check for null inputs in your own code to avoid unexpected failures in production.

java datetimeformatter: Practical Usage and Code Examples | RYUSLOG DEV