【Spring Framework】String型のフォーム値が未入力の場合nullで受け取る方法

Spring Frameworkで未入力のフォームの値をデフォルトで設定されている空文字ではなく、nullで受け取る方法をまとめます。

環境は以下です。

  • Spring Boot 2.3.1.RELEASE

  • Thymeleaf 3.0.11.RELEASE

  • Kotlin 1.3.70

以下のようなフォームと、フォームの値を格納するクラスを用意して説明していきます。

        <form method="post" action="/todo" th:object="${todoCreateForm}">
            <div class="form-group">
                <label th:for="*{title}">Title</label>
                <input type="text" class="form-control" name="title">
                <p class="alert alert-danger" th:if="${#fields.hasErrors('title')}" th:errors="*{title}"></p>
            </div>
            <div class="form-group">
                <label th:for="*{detail}">Detail</label>
                <textarea class="form-control" name="detail"></textarea>
                <p class="alert alert-danger" th:if="${#fields.hasErrors('detail')}" th:errors="*{detail}"></p>
            </div>
            <button type="submit" class="btn btn-primary"> create</button>
        </form>
data class TodoCreateForm(

        @field:NotNull
        val title: String? = null,


        @field:NotNull
        val detail: String? = null
)

このときデフォルトの挙動では、未入力の項目は空文字として扱われるため、下の画像のように@NotNullのバリデーションエラーにならずに、その後の処理が進んでいきます。

f:id:dais39:20200629224447p:plain

今回の要件であれば@NotEmptyを使うことで問題を解消することはできますが、例えば必須ではない入力項目に対して、@Patternを使った形式チェックをしたい場合などでは、常に空文字を許容できる正規表現を書く必要があります。

こういったケースのように未入力のフォーム値を空文字ではなくnullで扱うためには、以下のようにDataBinderに対象の型とPropertyEditorを設定します。

@Controller
@RequestMapping("todo")
class TodoController(
        private val todoQueryService: TodoQueryService,
        private val todoCreateService: TodoCreateService,
        private val todoDeleteService: TodoDeleteService
) {

    @InitBinder()
    fun allowEmptyDataBinding(binder: WebDataBinder) {
        binder.registerCustomEditor(String::class.java, StringTrimmerEditor(true))
    }

    // 省略
}

今回はStringの値を空文字ではなくnullで扱いたいのでWebDataBinder#registerCustomEditor()の第一引数にStringを、第二引数にSpringが提供しているStringTrimmerEditorを引数にtrueを指定して作成したインスタンスを設定します。

StringTrimmerEditorのソースコードは以下のようになっています。(一部省略)

// 省略
public class StringTrimmerEditor extends PropertyEditorSupport {

    @Nullable
    private final String charsToDelete;

    private final boolean emptyAsNull;


    /**
    * Create a new StringTrimmerEditor.
    * @param emptyAsNull {@code true} if an empty String is to be
    * transformed into {@code null}
    */
    public StringTrimmerEditor(boolean emptyAsNull) {
        this.charsToDelete = null;
        this.emptyAsNull = emptyAsNull;
    }


    @Override
    public void setAsText(@Nullable String text) {
        if (text == null) {
            setValue(null);
        }
        else {
            String value = text.trim();
            if (this.charsToDelete != null) {
                value = StringUtils.deleteAny(value, this.charsToDelete);
            }
            if (this.emptyAsNull && value.isEmpty()) {
                setValue(null);
            }
            else {
                setValue(value);
            }
        }
    }

    // 省略

}

StringTrimmerEditorのソースコードは以下のようになっていて、上記でStringTrimmerEditorに渡した引数trueはemptyAsNullというフィールドの値として処理されます。 emptyAsNullの値にtrueを設定することで、setAsText()の中の処理で空文字をnullとして設定するようになります。

実際に、サンプルアプリケーションではこれを設定することで、未入力のフォーム値の場合には値にnullが入り、@NotNullのバリデーションエラーによってエラーメッセージが表示されます。

f:id:dais39:20200629230021p:plain

今回はStringTrimmerEditorを使いましたが、Springでは他にもさまざまなPropertyEditorが提供されていて、公式ドキュメントに概要がまとめられているので、以下をみると面白いかもしれません。

https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-beans-conversion

参考

StringTrimmerEditor (Spring Framework 5.2.7.RELEASE API)

spring-framework/StringTrimmerEditor.java at master · ndimiduk/spring-framework · GitHub

This entry is released under version 2.0 of the Apache License.