Fix java.time.format.DateTimeParseException: Invalid value for MonthOfYear in Java

intermediateโ˜• Java2026-06-12| Java 8+, any OS (Windows / Linux / macOS), any framework using java.time API

Error Message

java.time.format.DateTimeParseException: Text '2023-13-01' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13
#java#datetime#date-format

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 is yyyy-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) vs MM/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.

Related Error Notes