We format LocalDateTime in Java by calling its format() method with a DateTimeFormatter, either a predefined one such as ISO_LOCAL_DATE_TIME or a custom pattern created with DateTimeFormatter.ofPattern(). The method returns a String, and the LocalDateTime itself does not change.
We format a LocalDateTime whenever a date and time leaves our code as text, for example in a log line, a CSV export, a file name or a confirmation message shown to a user.
The following example formats one meeting time in three ways.
LocalDateTime meetingAt = LocalDateTime.of(2026, 3, 5, 14, 30, 15);
String iso = meetingAt.toString(); // "2026-03-05T14:30:15"
String custom = meetingAt.format(DateTimeFormatter.ofPattern("dd-MM-uuuu HH:mm")); // "05-03-2026 14:30"
String twelveHour = meetingAt.format(DateTimeFormatter.ofPattern("MMM d, uuuu h:mm a", Locale.US)); // "Mar 5, 2026 2:30 PM"
Notice that the pattern alone decides the shape of the text, including the 12-hour clock and the month name. We start with the default toString() output, move to predefined and custom formatters, and finish with the pattern mistakes and time zone errors that show up in real projects.
1. Default Format With LocalDateTime.toString()
When we print a LocalDateTime or concatenate it into a string, Java calls toString(), which returns an ISO-8601 text with a T between the date and the time. The length of that text depends on the value, because toString() leaves out the parts that are zero.
| Value | toString() result | Shape |
|---|---|---|
| 14:30 with zero seconds | 2026-03-05T14:30 | uuuu-MM-dd’T’HH:mm |
| 14:30:15 | 2026-03-05T14:30:15 | uuuu-MM-dd’T’HH:mm:ss |
| 14:30:15 and 500 ms | 2026-03-05T14:30:15.500 | uuuu-MM-dd’T’HH:mm:ss.SSS |
| 14:30:15 and 123456 micros | 2026-03-05T14:30:15.123456 | uuuu-MM-dd’T’HH:mm:ss.SSSSSS |
| 14:30:15 and 123456789 nanos | 2026-03-05T14:30:15.123456789 | uuuu-MM-dd’T’HH:mm:ss.SSSSSSSSS |
A booking app that exports start times to CSV runs into this problem. A slot starting at 14:30 becomes 2026-03-05T14:30 while the slot at 14:30:15 gets eight more characters, so a spreadsheet or a fixed-width parser on the other side sees two different formats in one column.
LocalDateTime slotStart = LocalDateTime.of(2026, 3, 5, 14, 30);
String shortText = slotStart.toString(); // "2026-03-05T14:30"
String fullText = slotStart.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); // "2026-03-05T14:30:00"
For text that another program reads, we use an explicit formatter instead of toString(), because the formatter always prints the seconds. We keep toString() for debugging and log messages read by people.
2. The LocalDateTime.format() Method
The method format(DateTimeFormatter) takes a formatter and returns the formatted String. It throws a DateTimeException when the formatter asks for a field that a LocalDateTime does not have, such as a time zone, as we will see in section 6.
A DateTimeFormatter is immutable and thread-safe, so we create it once and keep it in a static final field. Building the formatter parses the pattern, and doing that for every log line wastes work.
static final DateTimeFormatter LOG_FORMAT = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
The call LOG_FORMAT.format(value) gives the same result as value.format(LOG_FORMAT), so we pick whichever reads better in the code around it.
LocalDateTime loggedAt = LocalDateTime.of(2026, 3, 5, 9, 5, 7);
String viaValue = loggedAt.format(LOG_FORMAT); // "2026-03-05 09:05:07"
String viaFormatter = LOG_FORMAT.format(loggedAt); // "2026-03-05 09:05:07"
3. Predefined ISO Formatters
The DateTimeFormatter class has constants for the common ISO formats. Some of them print only part of the value, so we can get the date or the time from a LocalDateTime without a custom pattern.
LocalDateTime meetingAt = LocalDateTime.of(2026, 3, 5, 14, 30, 15);
String isoDateTime = meetingAt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); // "2026-03-05T14:30:15"
String isoDate = meetingAt.format(DateTimeFormatter.ISO_LOCAL_DATE); // "2026-03-05"
String isoTime = meetingAt.format(DateTimeFormatter.ISO_LOCAL_TIME); // "14:30:15"
String basicDate = meetingAt.format(DateTimeFormatter.BASIC_ISO_DATE); // "20260305"
String weekDate = meetingAt.format(DateTimeFormatter.ISO_WEEK_DATE); // "2026-W10-4"
String ordinalDate = meetingAt.format(DateTimeFormatter.ISO_ORDINAL_DATE); // "2026-064"
The constants ISO_DATE_TIME and ISO_DATE also work, because their offset and zone parts are optional. Three other constants require an offset or a zone, which a LocalDateTime does not have, so they throw at runtime.
| Formatter | Result for a LocalDateTime |
|---|---|
| ISO_OFFSET_DATE_TIME | UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds |
| RFC_1123_DATE_TIME | UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds |
| ISO_INSTANT | UnsupportedTemporalTypeException: Unsupported field: InstantSeconds |
LocalDateTime meetingAt = LocalDateTime.of(2026, 3, 5, 14, 30, 15);
String withOffset = meetingAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); // UnsupportedTemporalTypeException: Unsupported field: OffsetSeconds
4. Format LocalDateTime With Custom Patterns
When no predefined formatter fits, we write a pattern and pass it to DateTimeFormatter.ofPattern(). Each letter in the pattern stands for one field of the date or time, the number of letters sets the width or the text style, and text in single quotes is copied as it is.

A support ticket system shows the same LocalDateTime in several places, and each place wants its own pattern. Each line of the snippet serves one of those places.
LocalDateTime createdAt = LocalDateTime.of(2026, 3, 5, 14, 30, 15, 123_000_000);
String dbStyle = createdAt.format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")); // "2026-03-05 14:30:15"
String europe = createdAt.format(DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm")); // "05/03/2026 14:30"
String usStyle = createdAt.format(DateTimeFormatter.ofPattern("MM/dd/uuuu hh:mm a", Locale.US)); // "03/05/2026 02:30 PM"
String withMillis = createdAt.format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss.SSS")); // "2026-03-05 14:30:15.123"
String fileName = createdAt.format(DateTimeFormatter.ofPattern("'report_'uuuuMMdd_HHmmss'.csv'")); // "report_20260305_143015.csv"
String longText = createdAt.format(DateTimeFormatter.ofPattern("EEEE, MMMM d 'at' HH:mm", Locale.US)); // "Thursday, March 5 at 14:30"
The pattern uuuu-MM-dd HH:mm:ss is also the answer to the common question of how to remove the T from the ISO text. The full list of letters is in the DateTimeFormatter guide, and the 12-hour format article covers the AM/PM letters in more depth.
4.1. Pattern Letters That Print the Wrong Value
A wrong pattern letter does not throw an exception. The code runs, and the wrong value shows up later in a report or in a customer email, so these four mistakes are worth checking in every pattern.
- The letter Y is the week-based year, not the calendar year. On the last days of December it prints the next year.
- The letter m is the minute and M is the month, so dd-mm-uuuu puts the minutes where the month should be.
- The letter h is the 12-hour clock and H is the 24-hour clock. A pattern with hh and no a prints 02:30 for both night and afternoon.
- Text letters such as MMM, EEEE and a use the default locale of the JVM, so the same code prints “Dezember” on a server with a German locale.
LocalDateTime yearEnd = LocalDateTime.of(2025, 12, 29, 14, 30);
String weekYear = yearEnd.format(DateTimeFormatter.ofPattern("YYYY-MM-dd")); // "2026-12-29"
String calendarYear = yearEnd.format(DateTimeFormatter.ofPattern("uuuu-MM-dd")); // "2025-12-29"
String minuteAsMonth = yearEnd.format(DateTimeFormatter.ofPattern("dd-mm-uuuu")); // "29-30-2025"
String german = yearEnd.format(DateTimeFormatter.ofPattern("d MMMM uuuu", Locale.GERMANY)); // "29 Dezember 2025"
We pass a Locale to ofPattern() whenever the pattern contains text fields. The letters yyyy and uuuu print the same value for any year after year 0, so both are fine for formatting, and the strict parsing article explains why uuuu matters when we parse.
5. Locale-Specific Date-Time Styles
Instead of writing a pattern, we can ask for the standard format of a country with DateTimeFormatter.ofLocalizedDateTime() and a FormatStyle. Java takes the patterns from the CLDR locale data, so a page shown to users in many countries gets the order and separators each user expects.
LocalDateTime meetingAt = LocalDateTime.of(2026, 3, 5, 14, 30, 15);
String uk = meetingAt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.UK)); // "5 Mar 2026, 14:30:15"
String germany = meetingAt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).withLocale(Locale.GERMANY)); // "05.03.26, 14:30"
String longStyle = meetingAt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG).withLocale(Locale.UK)); // DateTimeException: Unable to extract ZoneId from temporal 2026-03-05T14:30:15
The styles LONG and FULL include the time zone name, so they throw a DateTimeException for a LocalDateTime. Since JDK 20, the US English time also uses a narrow no-break space (U+202F) before AM or PM, which breaks tests that compare the text with a plain space. The locale-based formatting article covers more locales.
6. Adding a Time Zone to the Formatted Text
A LocalDateTime has no zone and no offset, so a pattern with z, VV, X or O throws a DateTimeException. To print a zone, we first attach one with atZone(), which returns a ZonedDateTime with the same date and time, and format that value.
LocalDateTime meetingAt = LocalDateTime.of(2026, 3, 5, 14, 30, 15);
String noZone = meetingAt.format(DateTimeFormatter.ofPattern("HH:mm z")); // DateTimeException: Unable to extract ZoneId from temporal 2026-03-05T14:30:15
ZonedDateTime inParis = meetingAt.atZone(ZoneId.of("Europe/Paris"));
String withZone = inParis.format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm z")); // "2026-03-05 14:30 CET"
String withOffset = inParis.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); // "2026-03-05T14:30:15+01:00"
The call atZone() only labels the local time with a zone, it does not convert it. If the LocalDateTime was read in Paris, atZone(ZoneOffset.UTC) prints the Paris time with a Z at the end, which is wrong by one or two hours. To show the same moment in another zone, we use withZoneSameInstant(), as described in LocalDateTime to ZonedDateTime and formatting ZonedDateTime.
7. Parsing the String Back to LocalDateTime
The reverse operation uses the same formatter with LocalDateTime.parse(). Text that does not match the pattern throws a DateTimeParseException, so input from users or files is parsed inside a try-catch block.
DateTimeFormatter dbFormat = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
LocalDateTime parsed = LocalDateTime.parse("2026-03-05 14:30:15", dbFormat); // 2026-03-05T14:30:15
LocalDateTime bad = LocalDateTime.parse("05/03/2026 14:30", dbFormat); // DateTimeParseException: Text '05/03/2026 14:30' could not be parsed at index 0
In real code, we return an Optional or a validation error instead of letting the exception reach the user.
static Optional<LocalDateTime> parseOrEmpty(String text, DateTimeFormatter formatter) {
try {
return Optional.of(LocalDateTime.parse(text.strip(), formatter));
} catch (DateTimeParseException e) {
return Optional.empty();
}
}
DateTimeFormatter dbFormat = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
Optional<LocalDateTime> good = parseOrEmpty(" 2026-03-05 14:30:15 ", dbFormat); // Optional[2026-03-05T14:30:15]
Optional<LocalDateTime> wrong = parseOrEmpty("tomorrow", dbFormat); // Optional.empty
The LocalDateTime parsing article covers optional sections and parsing with default values. For dates without a time, the same ideas apply to formatting LocalDate.
8. LocalDateTime Formatting FAQs
After the first working pattern, the next questions concern JSON output in Spring Boot and sharing one formatter between threads.
8.1. How do we format LocalDateTime as yyyy-MM-dd HH:mm:ss?
We pass the pattern to ofPattern() and call format(). We prefer uuuu over yyyy when the same formatter also parses input, and both print the same year.
LocalDateTime orderedAt = LocalDateTime.of(2026, 7, 1, 8, 0);
String text = orderedAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); // "2026-07-01 08:00:00"
8.2. Why does LocalDateTime.toString() drop the seconds?
The method toString() prints the shortest ISO-8601 form, so it leaves out seconds and fractions that are zero. The formatter ISO_LOCAL_DATE_TIME always prints the seconds, which gives a fixed length, as shown in section 1.
8.3. Is DateTimeFormatter thread-safe?
Yes. A DateTimeFormatter is immutable, so one static final instance can serve all threads. The old SimpleDateFormat is not thread-safe, and sharing one instance between threads produces wrong dates.
8.4. How do we format LocalDateTime in a Spring Boot JSON response?
Jackson writes a LocalDateTime as an ISO string such as 2026-03-05T14:30:15 when the JSR-310 module is registered, which Spring Boot does for us. For another shape, we annotate the field with @JsonFormat(pattern = “uuuu-MM-dd HH:mm”). The Jackson date-time error article explains the error we get without the module.
8.5. Can we format LocalDateTime with the timezone of the server?
Yes, with atZone(ZoneId.systemDefault()) before formatting. We do that only when the LocalDateTime holds server time, because the server zone is often UTC in containers and cloud machines.
9. Conclusion
We format a LocalDateTime with format() and a DateTimeFormatter that we keep in a constant. The predefined ISO formatters cover machine-readable text, ofPattern() covers any custom layout, and ofLocalizedDateTime() gives each country its own format.
Most bugs come from the pattern, not from the API. We use u or y instead of Y, pass a Locale for text fields, and attach a zone with atZone() before we print one.
10. References
Happy Learning !!
How to arrive at this date format 2019-11-12T06:00:47.588-0600 using date and time formatter
Use this custom pattern : yyyy-MM-dd’T’HH:mm:ss.SSSz