The Error
java.time.format.DateTimeParseException: Text '2023-13-01' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13
Production is down, logs are flooding with DateTimeParseException, and somewhere upstream a date string with month 13 is crashing your parser. The JVM is right โ month 13 doesn't exist. But the real culprit is almost never a truly invalid date. It's a format mismatch: your code is reading the day slot as the month slot.
The input 2023-13-01 is structured as yyyy-dd-MM (year-day-month). Java's default parser โ and most DateTimeFormatter patterns โ expect yyyy-MM-dd. Day 13 lands in the month position, and the parse blows up.
Root Causes
- Format mismatch: Pattern says
yyyy-MM-dd, input isyyyy-dd-MM. This is by far the most common cause. - Bad upstream data: An external API, CSV import, or legacy database column serializes dates in a non-ISO format โ and nobody validated it on the way in.
- European vs American date order:
dd/MM/yyyy(EU) vsMM/dd/yyyy(US). When the day value exceeds 12, the parse fails immediately and loudly. - Unvalidated user input: Someone typed it wrong and it slipped through to the parser unchecked.
Step-by-Step Fix
Step 1: Figure out the actual format of your input
Don't touch the code yet. First, determine what format your string actually is. If you're receiving 2023-13-01, the 13 must be a day โ days go up to 31, months stop at 12. That means the pattern is yyyy-dd-MM. Log the raw input right before the parse call if the source isn't obvious.
Step 2: Match your formatter to the input structure
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateParseExample {
public static void main(String[] args) {
String input = "2023-13-01";
// WRONG โ default ISO parser expects yyyy-MM-dd
// LocalDate.parse(input); // throws DateTimeParseException!
// CORRECT โ pattern matches the actual input structure
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-dd-MM");
LocalDate date = LocalDate.parse(input, formatter);
System.out.println(date); // 2023-01-13
}
}
Step 3: Wrap parsing in try-catch for untrusted input
Dates arriving from users, APIs, or files should never be parsed bare. Wrap them:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public LocalDate parseDateSafely(String raw, String pattern) {
try {
return LocalDate.parse(raw, DateTimeFormatter.ofPattern(pattern));
} catch (DateTimeParseException e) {
// Log the raw value โ you'll need it to trace the data source
log.error("Failed to parse date '{}' with pattern '{}': {}", raw, pattern, e.getMessage());
throw new IllegalArgumentException("Invalid date format: " + raw, e);
}
}
Step 4: Handle multiple possible input formats
This scenario is common when pulling data from multiple sources. CSV exports, legacy APIs, and third-party integrations rarely agree on a date format:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
public LocalDate parseFlexible(String raw) {
List<String> patterns = List.of(
"yyyy-MM-dd", // ISO 8601 โ try first
"yyyy-dd-MM", // inverted day/month
"dd/MM/yyyy", // European
"MM/dd/yyyy", // American
"dd-MM-yyyy"
);
for (String pattern : patterns) {
try {
return LocalDate.parse(raw, DateTimeFormatter.ofPattern(pattern));
} catch (DateTimeParseException ignored) {
// try next
}
}
throw new IllegalArgumentException("Cannot parse date string: " + raw);
}
Caution: Dates like 2023-05-06 are ambiguous โ they match both yyyy-MM-dd (May 6) and yyyy-dd-MM (June 5). Order your patterns from most expected to least, and document which format each data source actually uses.
Step 5: Validate at the API boundary (Spring Boot)
REST endpoints are the cleanest place to block bad input. Let Spring reject it before it ever reaches your service layer:
@RestController
public class ReportController {
@GetMapping("/reports")
public List<Report> getReports(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to
) {
// Spring returns 400 Bad Request automatically for invalid dates
// Your service never sees malformed input
return reportService.find(from, to);
}
}
Verify the Fix
Run this snippet to confirm the problematic input now parses correctly:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class VerifyFix {
public static void main(String[] args) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-dd-MM");
LocalDate result = LocalDate.parse("2023-13-01", fmt);
System.out.println("Parsed: " + result); // Parsed: 2023-01-13
System.out.println("Month: " + result.getMonthValue()); // Month: 1
System.out.println("Day: " + result.getDayOfMonth()); // Day: 13
assert result.equals(LocalDate.of(2023, 1, 13)) : "Unexpected result";
System.out.println("Fix confirmed.");
}
}
Expected output:
Parsed: 2023-01-13
Month: 1
Day: 13
Fix confirmed.
Tips
Always log the raw value before parsing
At 2 AM, the most painful part of this error is figuring out where the bad string came from โ not fixing it. One log line before every parse call on untrusted input saves you that pain:
log.debug("Parsing date string: '{}'", rawDateString);
LocalDate date = LocalDate.parse(rawDateString, formatter);
Use ResolverStyle.STRICT for stricter validation
DateTimeFormatter runs in SMART mode by default. That means it silently clamps out-of-range values instead of throwing โ February 30 quietly becomes February 28, with no warning. Switch to STRICT to catch those cases:
import java.time.format.ResolverStyle;
DateTimeFormatter strictFormatter = DateTimeFormatter
.ofPattern("uuuu-MM-dd") // 'u' instead of 'y' required for STRICT mode
.withResolverStyle(ResolverStyle.STRICT);
LocalDate date = LocalDate.parse("2023-02-30", strictFormatter); // throws exception
Debugging timestamp conversions
When tracing where a bad date originated โ especially from Unix timestamps or external APIs โ I keep ToolCraft's Timestamp Converter open in a tab. Paste in a Unix epoch or an ISO string, and it shows the human-readable breakdown instantly without uploading anything. Useful when cross-referencing what the data source actually sent versus what your parser expected.
Standardize on ISO 8601 at system boundaries
If you control the API contract, enforce yyyy-MM-dd in your OpenAPI spec or documentation. Accepting multiple formats feels flexible now. Six months later, you'll be debugging another format mismatch at midnight.

