0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日付入力とValidation

0
Last updated at Posted at 2026-08-13

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のときに入力チェックを実施し、エラーがあるときはリダイレクトして入力画面へ戻します。

SampleController.java
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 で実装クラス指定します。

TransferDateConstraint.java
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で指定し、存在しない日付だった場合に入力チェックエラーとしています。

TransferDateValidator.java
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 でテストを実装し、日付の柔軟性と実行結果を確認するテストケースです。

TransferDateSpec.groovy
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)

index.html
<!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>

image.png

2028/02/29の場合:OK
image.png

2027/02/29の場合:存在しないため入力チェックエラー
image.png

サンプルコード

https://github.com/A-pZ/spring-sample-date-lenient

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?