Javaアプリケーションへ入力した日付をチェックする方法
Javaで日時を扱うパッケージ : java.time の、日付に関するクラスと入力値のチェック方法です。デフォルトの動作も知っておきましょう。
このドキュメントでは、ロケールは日本を前提にしています。
日付のフォーマットと、日付の誤りをどう扱うか
アプリケーションが受け取った日付の入力を日付の型で扱うときや、日付を出力するときの文字列を定義するのが日付のフォーマッターで、もし存在しない日付を指定したときの振る舞いを定めるのが、日付フォーマットの柔軟性です。
フォーマッター: java.time.format.DateTimeFormatter
例) フォーマッターで "uuuu-MM-dd" と定義しておき、2027-01-31の文字列を渡すと、日付型:2027年1月31日になる。
String value = "2027-01-31";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu/MM/dd");
Localdate result = formatter.parse(value, LocalDate::from);
フォーマットの柔軟性: java.time.format.ResolverStyle
フォーマットの柔軟性では、もし存在しない日付が入力されたときの動きを決めます。
| ResolverStyle | 概要 |
|---|---|
| SMART | 月によって存在しない日が入力されても月末日になる。デフォルトの設定。 |
| STRICT | java.time.format.DateTimeParseExceptionをスローする |
| LENIENT | 存在しない月や日だった場合、その超えた数だけ加算する |
SMART(デフォルト)
月によって存在しない日が入力されても月末日になる。ただし日付の値は31まで。
例1) 2026/2/29 → 2026/2/28
例2) 2028/2/29 → うるう年で存在する日付なので、2028/2/29のまま
例3) 2028/1/32 → java.time.format.DateTimeParseException をスローする
STRICT
存在しない日付を入力した場合は java.time.format.DateTimeParseException をスローする
例1) 2026/2/29 → 存在しない日付なので例外をスロー
例2) 2028/2/29 → うるう年で存在する日付なので、2028/2/29のまま
LENIENT
存在しない月や日だった場合、その超えた数だけ加算する
例1) 2026/2/29 → 2026/2/28 から1日増えているので、2026/2/28 の翌日=2026/3/1 を返す
例2) 2028/2/29 → うるう年で存在する日付なので、2028/2/29のまま
例3) 2026/15/1 → 2026/12 から 3か月増えているので、2027/3/1
柔軟性に関わらずエラーになる日付
- 日の値に1~31以外の値を設定
- 月の値に1~12以外の値を設定
柔軟性の設定に関わらず、この場合がいずれも java.time.format.DateTimeParseException をスローします。
SpringMVCのValidatorに適用する
入力した日付項目に日付入力の入力チェックを適用します。入力内容をFormクラスで受け取るControllerの実装例は以下です。POSTのときに入力チェックを実施し、エラーがあるときはリダイレクトして入力画面へ戻します。
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/")
public class SampleController {
@ModelAttribute
SampleForm sampleForm() {
return new SampleForm();
}
@GetMapping("")
public ModelAndView display(ModelAndView mnv, Model model, @ModelAttribute SampleForm sampleForm) {
if (model.containsAttribute("errors")) {
mnv.addObject(BindingResult.MODEL_KEY_PREFIX + "sampleForm", model.getAttribute("errors"));
}
mnv.setViewName("index");
return mnv;
}
@PostMapping("")
public ModelAndView check(ModelAndView mnv, @ModelAttribute @Validated SampleForm sampleForm, BindingResult bindingResult, RedirectAttributes redirectAttributes ) {
if (bindingResult.hasFieldErrors()) {
redirectAttributes.addFlashAttribute("errors", bindingResult);
redirectAttributes.addFlashAttribute("sampleForm", sampleForm);
mnv.setViewName("redirect:/");
return mnv;
}
mnv.addObject("message", "check ok");
mnv.setViewName("index");
return mnv;
}
}
入力を扱うFormクラスです。日付は transferDate で受け取ります。入力チェックの定義は、@TransferDateConstraint で定義します。
import com.github.apz.sample.controller.constraints.TransferDateConstraint;
import lombok.Data;
@Data
public class SampleForm {
@TransferDateConstraint
private String transferDate;
}
@TransferDateConstraint アノテーションで入力チェックの宣言を行い、チェック内容を @Constraint の validatedBy で実装クラス指定します。
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Retention(RUNTIME)
@Target({FIELD, METHOD})
@Constraint(validatedBy = TransferDateValidator.class)
public @interface TransferDateConstraint {
String message() default "日付が正しくありません";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
先ほどの validatedBy で実装クラスを宣言した TransferDateValidator にて入力チェックエラーの有無を判定します。Validatorクラスは jakarta.validation.ConstraintValidator の isValid メソッドに入力チェックを実装します。
以下の例は柔軟性をSTRICTで指定し、存在しない日付だった場合に入力チェックエラーとしています。
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Objects;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import com.github.apz.sample.model.TransferDate;
public class TransferDateValidator implements ConstraintValidator<TransferDateConstraint, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (Objects.isNull(value) || value.isBlank()) {
return true;
}
try {
TransferDate transferDate = TransferDate.of(value);
transferDate.toLocalDate(ResolverStyle.STRICT);
} catch (DateTimeParseException ex) {
return false;
}
return true;
}
}
テストコード
以下の例は、spockframework でテストを実装し、日付の柔軟性と実行結果を確認するテストケースです。
import java.time.format.DateTimeParseException
import java.time.format.ResolverStyle
import spock.lang.Specification
import spock.lang.Unroll
@Unroll
class TransferDateSpec extends Specification {
def "SMART"() {
given:
TransferDate tranferDate = TransferDate.of($value);
when:
LocalDate toLocalDate = tranferDate.toLocalDate(ResolverStyle.SMART)
then:
toLocalDate.getYear() == _year
toLocalDate.getMonthValue() == _month
toLocalDate.getDayOfMonth() == _day
where:
$value || _year | _month | _day
'2026/02/28' || 2026 | 2 | 28
'2026/02/29' || 2026 | 2 | 28
'2028/02/29' || 2028 | 2 | 29
'2028/02/30' || 2028 | 2 | 29
'2028/04/31' || 2028 | 4 | 30
}
def "STRICT"() {
given:
TransferDate tranferDate = TransferDate.of($value);
when:
LocalDate toLocalDate = tranferDate.toLocalDate(ResolverStyle.STRICT)
then:
toLocalDate.getYear() == _year
toLocalDate.getMonthValue() == _month
toLocalDate.getDayOfMonth() == _day
where:
$value || _year | _month | _day
'2026/02/28' || 2026 | 2 | 28
}
def "STRICT_ERRORS"() {
given:
TransferDate tranferDate = TransferDate.of($value);
when:
LocalDate toLocalDate = tranferDate.toLocalDate(ResolverStyle.STRICT)
then:
thrown(DateTimeParseException)
where:
$value || _
'2026/02/29' || _
}
def "LENIENT"() {
given:
TransferDate tranferDate = TransferDate.of($value);
when:
LocalDate toLocalDate = tranferDate.toLocalDate(ResolverStyle.LENIENT)
then:
toLocalDate.getYear() == _year
toLocalDate.getMonthValue() == _month
toLocalDate.getDayOfMonth() == _day
where:
$value || _year | _month | _day
'2026/02/28' || 2026 | 2 | 28
'2026/02/29' || 2026 | 3 | 1
'2026/02/30' || 2026 | 3 | 2
'2028/02/29' || 2028 | 2 | 29
'2028/02/30' || 2028 | 3 | 1
}
def "ALL_ERRORS"() {
given:
TransferDate tranferDate = TransferDate.of($value);
when:
LocalDate toLocalDate = tranferDate.toLocalDate(ResolverStyle.SMART)
then:
thrown(DateTimeParseException)
where:
$value || _
'2026/02/32' || _
'2026/03/00' || _
'2026/00/01' || _
'2026/13/01' || _
}
}
画面(Thymeleaf)
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>サンプルフォーム</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="row justify-content-center">
<div class="alert alert-primary" role="alert" th:if="${message}" th:text="${message}">
</div>
<div class="alert alert-warning" role="alert" th:if="${warning}" th:text="${warning}"></div>
<div class="col-md-8 col-lg-6">
<h1 class="h4 fw-normal mb-4">日付入力フォーム</h1>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form th:action="@{/}" th:object="${sampleForm}" method="post">
<div class="mb-3">
<label for="transferDate" class="form-label small text-secondary">
転送日付 <span class="text-danger">*</span>
</label>
<input type="text" class="form-control" id="transferDate" th:field="*{transferDate}"
placeholder="YYYY/mm/dd"
th:classappend="${#fields.hasErrors('transferDate')} ? 'is-invalid' : ''">
<div class="invalid-feedback" th:errors="*{transferDate}"></div>
</div>
<!-- ボタン -->
<div class="d-flex justify-content-end gap-2">
<button type="reset" class="btn btn-outline-secondary btn-sm px-4">
リセット
</button>
<button type="submit" class="btn btn-primary btn-sm px-4">
送信
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
2027/02/29の場合:存在しないため入力チェックエラー


