Springで日付・数値・Enumを安全にバインド!Formatterで入力エラーを防ぐ方法
生徒
「Springで日付や数値を入力させたときに、うまくバインドされないことがあるんですが、どうすればいいですか?」
先生
「Springでは、Formatterを使うことで、日付や数値、Enumの入力フォーマットを自由に定義できて、バインド時のエラーも減らせますよ。」
生徒
「Enumも文字列で渡すとエラーになりますよね?そのあたりもFormatterで対応できるんですか?」
先生
「はい、Formatterを使えばEnumの日本語表示も含めて制御できます。それでは実際に、日付・数値・Enumのバインド方法を丁寧に見ていきましょう。」
1. Springのバインドとは?
Spring MVCでは、フォームから送信された文字列データをコントローラーの引数やフォームオブジェクトのフィールドにバインド(紐づけ)します。日付(LocalDateやDate)、数値(IntegerやBigDecimal)、列挙型(Enum)などにバインドするには、型に応じた変換が必要です。
2. なぜFormatterが必要なのか?
Springのデフォルト設定でもある程度はバインドできますが、以下のような課題があります:
- 日付形式が「yyyy/MM/dd」だとエラーになる
- 数値に「カンマ付き」や「全角数字」が混ざるとエラー
- Enumに日本語や別名で表示したいが変換できない
こうした問題を解決するのがFormatterの役割です。独自のフォーマット定義で、入力エラーを防ぎ、ユーザーに優しい入力体験を提供できます。
3. 日付のFormatter実装例
まずは日付(LocalDate)を「yyyy/MM/dd」形式でバインドするFormatterを実装します。
public class LocalDateFormatter implements Formatter<LocalDate> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, formatter);
}
@Override
public String print(LocalDate object, Locale locale) {
return formatter.format(object);
}
}
そしてWebMvcConfigurerで登録します。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new LocalDateFormatter());
}
}
4. 数値のFormatter実装例
金額などでよく使われるBigDecimalに対して、「1,000.00」のようなフォーマットを扱えるFormatterを実装します。
public class BigDecimalFormatter implements Formatter<BigDecimal> {
private final DecimalFormat decimalFormat = new DecimalFormat("#,##0.00");
@Override
public BigDecimal parse(String text, Locale locale) throws ParseException {
Number number = decimalFormat.parse(text.replace(",", ""));
return new BigDecimal(number.toString());
}
@Override
public String print(BigDecimal object, Locale locale) {
return decimalFormat.format(object);
}
}
日付と同様にWebConfigに登録します。
5. EnumのFormatterで日本語表示
Enum型に対しても、Formatterで独自の変換が可能です。たとえば以下のようなGender列挙型を使ってみましょう。
public enum Gender {
MALE("男性"), FEMALE("女性");
private final String label;
Gender(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
public static Gender fromLabel(String label) {
for (Gender g : values()) {
if (g.label.equals(label)) return g;
}
throw new IllegalArgumentException("不正な値: " + label);
}
}
そしてFormatterを定義します。
public class GenderFormatter implements Formatter<Gender> {
@Override
public Gender parse(String text, Locale locale) {
return Gender.fromLabel(text);
}
@Override
public String print(Gender object, Locale locale) {
return object.getLabel();
}
}
このFormatterを登録することで、フォームで「男性」「女性」と表示して、内部的にはGender.MALEなどにバインドできます。
6. HTMLフォームとの連携
Formatterを使えば、HTMLフォームのinputやselectとも自然に連携できます。以下は日付と性別を入力するフォームの例です。
<form th:action="@{/submit}" th:object="${form}" method="post">
<div class="mb-3">
<label for="birthDate">生年月日</label>
<input type="text" th:field="*{birthDate}" class="form-control" placeholder="2025/09/04">
<div th:if="${#fields.hasErrors('birthDate')}" th:errors="*{birthDate}" class="text-danger"></div>
</div>
<div class="mb-3">
<label for="amount">金額</label>
<input type="text" th:field="*{amount}" class="form-control" placeholder="1,000.00">
<div th:if="${#fields.hasErrors('amount')}" th:errors="*{amount}" class="text-danger"></div>
</div>
<div class="mb-3">
<label for="gender">性別</label>
<select th:field="*{gender}" class="form-select">
<option value="">選択してください</option>
<option th:value="男性">男性</option>
<option th:value="女性">女性</option>
</select>
<div th:if="${#fields.hasErrors('gender')}" th:errors="*{gender}" class="text-danger"></div>
</div>
<button type="submit" class="btn btn-primary">送信</button>
</form>
7. 入力エラーの対策とバリデーション
Formatterを使うことで、日付や数値の形式に柔軟に対応できますが、それでも入力ミスは起こります。そのため、@ValidとBindingResultを使って、ユーザーに丁寧なエラーメッセージを返すことが重要です。
@PostMapping("/submit")
public String handleSubmit(@Valid @ModelAttribute UserForm form, BindingResult result, Model model) {
if (result.hasErrors()) {
return "form-page";
}
// 正常処理
return "success-page";
}
エラーメッセージは、バリデーションアノテーション(@NotNullや@Pastなど)と組み合わせて使うと効果的です。