A date string is valid when LocalDate.parse() can read it with a DateTimeFormatter in ResolverStyle.STRICT mode, so to check if a date is valid in Java, we try the parse. The call either returns a LocalDate, which means the text has the expected layout and the date exists in the calendar, or throws a DateTimeParseException, which we catch and treat as invalid.
We validate dates wherever they enter the app as text, such as an appointment date in a web form, a date column in an uploaded spreadsheet or a query parameter of a REST endpoint.
The following example checks four strings in the US layout MM-dd-uuuu with the isValidDate() method from section 1.
boolean leapDay = DateInput.isValidDate("02-29-2028"); // true, 2028 is a leap year
boolean noLeap = DateInput.isValidDate("02-29-2026"); // false
boolean april31 = DateInput.isValidDate("04-31-2026"); // false
boolean isoText = DateInput.isValidDate("2026-04-30"); // false, wrong layout
Notice that the method rejects the calendar errors as well as the wrong layout, because the formatter is strict. We also cover the ISO default, numbers from separate fields, business rules such as “not in the past”, several accepted layouts and the limits of regex.
1. LocalDate and DateTimeFormatter (Java 8 and Later)
A date check has two layers. Parsing tells us whether the text is a real date, and our own rules tell us whether that date is acceptable for the use case, for example not in the past.

1.1. Default Pattern yyyy-MM-dd
By default, LocalDate.parse() reads the ISO_LOCAL_DATE format yyyy-MM-dd, and this formatter is already strict. The format has three parts, separated by hyphens.
- Four digits or more for the year. Years outside 0000 to 9999 have a leading plus or minus sign.
- Two digits for the month of the year, padded with a zero, such as 02.
- Two digits for the day of the month, padded with a zero, such as 08.
LocalDate valid = LocalDate.parse("2023-02-08"); // 2023-02-08
int day = valid.getDayOfMonth(); // 8
LocalDate feb30 = LocalDate.parse("2023-02-30"); // DateTimeParseException: Invalid date 'FEBRUARY 30'
LocalDate oneDigit = LocalDate.parse("2023-2-8"); // DateTimeParseException at index 5
For input in the ISO format, we need no pattern at all. A try-catch around LocalDate.parse(text) is a complete check.
1.2. Custom Patterns with DateTimeFormatter
If the input has another layout, we create a formatter with DateTimeFormatter.ofPattern(). By default, that formatter uses ResolverStyle.SMART, which turns a 29th, 30th or 31st that the month does not have into the last day of the month, so 02-30-2023 passes as February 28. Values outside the field range, such as day 32 or month 13, throw an exception even in SMART mode.
DateTimeFormatter smart = DateTimeFormatter.ofPattern("MM-dd-uuuu");
LocalDate moved = LocalDate.parse("02-30-2023", smart); // 2023-02-28
LocalDate day32 = LocalDate.parse("01-32-2023", smart); // DateTimeParseException: Invalid value for DayOfMonth
For validation, we switch the formatter to ResolverStyle.STRICT and write the year as uuuu. With yyyy (year-of-era), a strict formatter rejects every date, as the ResolverStyle article explains.
A DateTimeFormatter is thread-safe and immutable, so we create one instance per pattern and share it across the app. The class keeps the formatter in a constant and offers two methods. The parse() method returns the date for callers that need it, and isValidDate() returns a plain boolean.
final class DateInput {
private static final DateTimeFormatter US_DATE =
DateTimeFormatter.ofPattern("MM-dd-uuuu").withResolverStyle(ResolverStyle.STRICT);
static Optional<LocalDate> parse(String text) {
if (text == null || text.isBlank()) {
return Optional.empty();
}
try {
return Optional.of(LocalDate.parse(text.strip(), US_DATE));
} catch (DateTimeParseException e) {
return Optional.empty();
}
}
static boolean isValidDate(String text) {
return parse(text).isPresent();
}
}
Optional<LocalDate> parsed = DateInput.parse("01-26-2023"); // Optional[2023-01-26]
Optional<LocalDate> feb30 = DateInput.parse("02-30-2023"); // Optional.empty
Optional<LocalDate> blank = DateInput.parse(" "); // Optional.empty
boolean nullText = DateInput.isValidDate(null); // false
Returning an Optional instead of null makes the invalid case visible in the method signature, and the method never prints a stack trace for bad user input, which is an expected event and not an error.
2. Validating Separate Day, Month and Year Values
Some forms send the date as three numbers, for example from three drop-down lists. We do not need a string at all in that case. LocalDate.of() throws a DateTimeException for a date that does not exist, and YearMonth.isValidDay() answers the question without an exception.
LocalDate fromFields = LocalDate.of(2028, 2, 29); // 2028-02-29
LocalDate badFields = LocalDate.of(2026, 2, 29); // DateTimeException: Invalid date 'February 29' as '2026' is not a leap year
boolean dayExists = YearMonth.of(2026, 4).isValidDay(31); // false
boolean leapYear = Year.isLeap(2028); // true
int daysInFeb = YearMonth.of(2026, 2).lengthOfMonth(); // 28
The method isValidDay() is also handy for filling a day drop-down with only the days the chosen month has. Leap years have their own article on how to check a leap year.
3. Checking Business Rules After Parsing
A real date is not always an acceptable date. Say a clinic app lets patients book an appointment online. The date must exist, must not be in the past, must be within the next 90 days, and the clinic is closed on Sundays.
We parse first and check the rules on the LocalDate. The method checkAppointment() receives today’s date as a parameter instead of calling LocalDate.now() inside, so tests can pass a fixed date; in production we pass LocalDate.now(clock) with the clinic’s time zone.
static String checkAppointment(String text, LocalDate today) {
Optional<LocalDate> parsed = DateInput.parse(text);
if (parsed.isEmpty()) {
return "not a valid date";
}
LocalDate date = parsed.get();
if (date.isBefore(today)) {
return "date is in the past";
}
if (date.isAfter(today.plusDays(90))) {
return "more than 90 days ahead";
}
if (date.getDayOfWeek() == DayOfWeek.SUNDAY) {
return "clinic is closed on Sundays";
}
return "ok";
}
LocalDate today = LocalDate.of(2026, 3, 14);
String monday = checkAppointment("03-16-2026", today); // "ok"
String sunday = checkAppointment("03-15-2026", today); // "clinic is closed on Sundays"
String past = checkAppointment("03-01-2026", today); // "date is in the past"
String tooFar = checkAppointment("07-01-2026", today); // "more than 90 days ahead"
String typo = checkAppointment("03-32-2026", today); // "not a valid date"
Each rule returns its own message, so the form can tell the patient what to fix. The same pattern works for a birth date (not in the future, age at least 18 with Period.between()) or an invoice date inside the current financial year. Comparing dates is covered in compare two LocalDate instances.
4. Accepting More Than One Format
When users type dates by hand, the same field may get 03-14-2026 and 03/14/2026. A pattern with optional sections in square brackets accepts both layouts and stays strict for each of them.
DateTimeFormatter either = DateTimeFormatter.ofPattern("[MM-dd-uuuu][MM/dd/uuuu]").withResolverStyle(ResolverStyle.STRICT);
LocalDate dash = LocalDate.parse("03-14-2026", either); // 2026-03-14
LocalDate slash = LocalDate.parse("03/14/2026", either); // 2026-03-14
LocalDate wrong = LocalDate.parse("02/30/2026", either); // DateTimeParseException: Invalid date 'FEBRUARY 30'
We accept only layouts that cannot be confused with each other. Adding dd/MM/uuuu next to MM/dd/uuuu would make 04/03/2026 valid in both, with two different meanings, so in that case we ask the user to pick a date with a date picker instead.
5. Why a Regular Expression Is Not Enough
A regular expression checks the shape of the text, which means the digits and the separators in the right places. It does not know that April has 30 days or that 2026 is not a leap year, so 04-31-2026 matches a typical date regex.
boolean shapeOk = Pattern.matches("\\d{2}-\\d{2}-\\d{4}", "04-31-2026"); // true
boolean realDate = DateInput.isValidDate("04-31-2026"); // false
A regex that handles month lengths and leap years exists, but it is long and hard to maintain. We use a regex only when we search for date-like text inside a larger string, as in the regex date format article, and let the strict formatter decide whether each match is a real date.
6. Best Practices for Java Date Validation
Most date validation bugs we find in code reviews come from a handful of habits, and each one has a simple fix.
- Use uuuu instead of yyyy in patterns. With STRICT mode it is required, and without it the code breaks as soon as someone adds STRICT later.
- Always use strict parsing with dtf.withResolverStyle(ResolverStyle.STRICT). The default SMART mode turns February 30 into February 28 without an error.
- Strict parsing solves the format problem only, so add range checks after parsing. For example, when importing financial records from a large spreadsheet, a date such as 01-15-2206 is valid but is a typing error for 2026.
- Create one formatter per pattern in a static final field, and return an Optional or a list of messages instead of null.
- Strip the input before parsing, because a trailing space from a copy-paste fails the parse.
Legacy code that still uses java.util.Date gets the same strict behavior from SimpleDateFormat.setLenient(false). In new code, LocalDate is the better choice, because its formatter is immutable and thread-safe.
7. Date Validation FAQs
Forms, REST APIs and legacy code each bring their own date validation cases.
7.1. How do we check if a date is valid in dd/MM/yyyy format?
We use the pattern dd/MM/uuuu with ResolverStyle.STRICT. The year letters uuuu accept the same four digits as yyyy, but work in strict mode.
DateTimeFormatter dayFirst = DateTimeFormatter.ofPattern("dd/MM/uuuu").withResolverStyle(ResolverStyle.STRICT);
LocalDate march = LocalDate.parse("14/03/2026", dayFirst); // 2026-03-14
7.2. How do we check if a date is in the past or the future?
We compare it with today’s date. The call date.isBefore(today) returns true for a past date, and date.isAfter(today) returns true for a future one. For “today or earlier”, we use !date.isAfter(today).
7.3. How do we validate a date field in a Spring Boot REST API?
We declare the field as LocalDate instead of String, so Jackson rejects an unparseable value with a 400 response. On top of that, Jakarta Bean Validation annotations such as @Past, @PastOrPresent and @Future work with LocalDate. The Spring bean validation example shows the setup, and Jackson dates covers the JSON format.
7.4. Is February 29 a valid date?
Only in leap years. LocalDate.parse() with a strict formatter accepts 02-29-2028 and rejects 02-29-2026, and Year.isLeap(year) answers the question for a year number.
8. Conclusion
A date string is valid when a strict DateTimeFormatter can parse it into a LocalDate. We create the formatter with uuuu and ResolverStyle.STRICT, catch DateTimeParseException in one helper method, and return an Optional or a boolean.
Parsing proves only that the date exists. Rules such as “not in the past” or “within 90 days” run on the parsed LocalDate, with today’s date passed in so the checks are testable. Our Java date and time tutorial lists the other date topics, from comparing dates to time zones.
9. References
- ResolverStyle (Java SE 25 API)
- LocalDate.parse() (Java SE 25 API)
- YearMonth.isValidDay() (Java SE 25 API)
- Jakarta Validation 3.1 specification
Happy Learning !!