見通しのValidation

Validationの実装方法

アノテーションを付けて、validate() を呼びます。

1. Form,Bodyにアノテーション

Hibernate Validator

Form, Body クラスに、Hibernate Validator のアノテーションを付けます。

e.g. Hibernate validator annotation in Form class @Java
public class SeaLandForm {

    @Length(max = 10)
    public String memberName;
}

必須チェックは @Required

必須チェックに関しては、LastaFlute で Required アノテーションを用意しています。 String や Integer など型を意識せずに利用できるアノテーションです。 (StringだからNotEmpty, IntegerだからNotNullなどと型ごとに使い分けなくてもいいように)

e.g. Required annotation in Form class @Java
public class SeaLandForm {

    @Required
    @Length(max = 10)
    public String memberName;
}

ルールは以下の通り。

String
NotBlank と同じ、nullと空文字と空白はダメ
Integer など
NotNull と同じ (必ず Wrapper 型 である Integer や Long を使いましょう)
LocalDate など
NotNull と同じ (必ず Java8 から導入された LocalDate などを使いましょう)
Boolean
NotNull と同じ (必ず Wrapper 型である Boolean を使いましょう)
List や Map など
NotEmpty と同じ、null や空リストはダメ、一件以上の要素が必要
Primitive型
効かない (0がsetされたのか、setされなくて0なのか区別が付かない)

全体的に プロパティの型にはWrapper型を使う というポリシーでよいでしょう。

システム項目は groups=ClientError.class

例えば、hiddenフィールドに保持しておいたIDやバージョン番号など、ユーザー入力ではないシステム項目は、 アノテーションの属性として groups=ClientError.class を指定するとよいでしょう。

e.g. Required annotation in Form class @Java
public class SeaLandForm {

    @Required(groups=ClientError.class)
    public Integer memberId;

    @Required
    @Length(max = 10)
    public String memberName;

    ...

    @Required(groups=ClientError.class)
    public Long versionNo;
}

これらの値が不正であれば、ユーザーの操作によるものではなくクライアントアプリのバグ (もしくは、ユーザーのいたずら) と考えられるので、 ユーザー向けメッセージではなく、400 Bad Request が戻ります。サーバー側ではINFOでログに残ります。 Assertの代わりのようなものと言えるでしょう

ネストのプロパティは @Valid

ネストしているクラスの中のアノテーションも有効にしたいときは、@Valid を付けます

e.g. how to validate nested class @Java
public class SeaLandForm {

    @Valid
    public PiariElement piari;

    @Valid
    public List<BonvoElement> bonvos;

    public static class PiariElement {

        @Required
        public String dstore;
    }

    public static class BonvoElement {
        ...
    }
}

ちなみに、再利用しないネストクラスは、Form や Body のインナークラスでの定義をオススメしています。 独立している必要もなく、見通しをよくするためにと。戻りの JsonResult も同じです。

2. Action で validate() を呼ぶ

メソッド呼び出し方式です!

Actionクラスで validate() メソッドを呼ぶと、アノテーションの通りにバリデーションされます。

e.g. validate() in Action class @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {}, () -> {
        return asHtml(path_Sea_SeaLandJsp);
    });
    ...
    return asHtml(path_Sea_SeaLandJsp); 
}
第一引数
アノテーションが付与されている Form もしくは Body
第二引数
アノテーションではできないバリデーションを実装 (後述)
第三引数
バリデーションエラーになったときのResponse処理 (いまの画面に戻すことがほとんど)

JsonResponse なら validateApi()

JsonResponseの場合は、validateApi() の方を使います。 こちらは第三引数がなく、バリデーションエラーは ApiFailureHook#handleValidationError() で、統一的なResponse処理がされます。

そもそも全部 JsonResponse なら

もし、そのアプリがJSON APIサーバーで全部 JsonResponse というときは、BaseAction が implements しているインターフェースを LaValidatableApi に変更すると、validate() 自体が JsonResponse 用のメソッドになります。(これは一番最初に決めましょう)

3. Lambdaで、もっとValidation

相関チェック、DB検索が必要なチェック

アノテーションではできないようなバリデーション (相関チェック、DB検索が必要なチェック) をする場合は、第二引数の Lambda で実装します。 そのとき、メッセージは messages のタイプセーフな add メソッドを使って指定します。

e.g. more validation in Lambda @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {
        if (isNotEmpty(form.sea) && isNotEmpty(form.land)) {
            if (form.sea.length() > form.land.length()) {
                messages.addErrorsSeaLand("sea");
            }
        }
    }, () -> {
        return asHtml(path_Sea_SeaLandJsp);
    });
    ...
}

エラーメッセージを追加する場合は、[App]_message.properties に追加して FreeGen を叩きます。

moreValidate() のススメ

ちょっとせまっくるしいので、moreValidate() メソッドに出すとよいでしょう。

e.g. moreValidate() @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    validate(form, messages -> {
        moreValidate(form, messages);
    }, () -> {
        return asHtml(path_Sea_SeaLandJsp);
    });
    ...
}

private void moreValidate(SeaLandForm form, HarborMessages messages) {
    if (isNotEmpty(form.sea) && isNotEmpty(form.land)) {
        if (form.sea.length() > form.land.length()) {
            messages.addErrorsSeaLand("sea");
        }
    }
}

アノテーションでエラーがあっても呼ばれる

アノテーションの方でバリデーションエラーが発生しても、第二引数の moreValidation の Lambda は必ず呼び出されます。 なので、必須項目でも必須チェックに引っかかって null になっているかもしれないので、実際に値が入っていたら、という分岐を入れましょう。

e.g. null check for more validation @Java
private void moreValidate(SeaLandForm form, HarborMessages messages) {
    if (isNotEmpty(form.sea) && isNotEmpty(form.land)) {
        if (form.sea.length() > form.land.length()) {
            messages.addErrorsSeaLand("sea");
        }
    }
}

エラーメッセージを調整

TODO jflute [App]_message.properties, FreeGen

番外. JsonResultのバリデーション

LastaDocに載るのでフロント側に伝えやすい

asJson() の引数に指定する JsonResult にも、アノテーションを付けることができます。

付けておくと、単なるサーバー側のバグチェックだけでなく、LastaDoc にアノテーションが表示されるので、 フロント側のディベロッパーに項目の特性を自然と伝えることができます。

e.g. Required annotation in Bean class @Java
public class SeaLandBean {

    @Required
    public Integer memberId;

    @Required
    public String memberName;

    ...
}
e.g. asJson() in Action class @Java
@Execute
public JsonResponse<SeaLandBean> index() {
    ...
    SeaLandBean bean = ...
    return asJson(bean); // validated in this method
}

これはユーザー入力でもなければクライアントエラーでもなく、サーバーのバグやデータ不備なので、エラーが発生したらシステムエラー (500 Server Error) です。Form や Body の ClientError と同様、Assertの代わりのようなものです。 なので、こちらではメッセージ調整は発生しません。

凝り過ぎてもつらいので、例えば Required だけチェックする というような割り切りがオススメです。フロントサイドのディベロッパーと相談してみましょう。

同じ JsonResult でも groups でバリデーションを分岐

同じ JSON Bean でも、状況によってバリデーションの内容が異なる場合は、Hibernate Validator の groups の機能を使うとよいでしょう。

例えば、mysticId はデフォルトのときに必須、Landが指定されたら onemanDate の方が必須(そのときは、mysticIdは必須ではない)、というようなときは以下のようになります。

e.g. groups in JSON Bean @Java
@Required
public Integer mysticId;

@Required(groups=Land.class)
public LocalDate onemanDate;
e.g. asJson() with groups in action @Java
@Execute
public JsonResponse<SeaLandBean> index() {
    ...
    SeaLandBean bean = ...
    boolean land = ...
    return asJson(bean).groupValidator(land ? Land.class : null);
}

本番ではチェックしない、とか、警告だけとか

TODO jflute

三大チェックポイント

気をつけて!int や boolean の @Required

Primitive型を使うと、setされたのかされてないのかが区別付かないため、@Required が効きません。

e.g. don't use primitive, use wrapper type @Java
public class SeaLandBean {

    @Required
    //public int piari; *Bad
    public Integer piari; // Good

    @Required
    //public boolean bonvo; *Bad
    public Boolean bonvo; // Good
}

特に、Boolean などはついつい boolean を使いたくなるので注意です。

全体的に プロパティの型にはWrapper型を使う というポリシーでよいでしょう。

忘れないで!ネストBean には @Valid

ネストした Bean には @Valid を定義し忘れないようにしましょう。

e.g. don't forget @Valid annotation for nested bean @Java
public class SeaLandBean {

    @Required
    @Valid
    public PiariElement piari;

    @Valid
    public List<BonvoElement> bonvos;

    public static class PiariElement {

        @Required
        public String dstore;
    }

    public static class BonvoElement {
        ...
    }
}

思い出して!List の @Required は一件以上

List に @Required を付けると、空リストでバリデーションエラーになります。 もし、"空リストでもOK だが null はあり得ない" とかであれば、@NotNull を付けましょう。

e.g. List @Valid annotation for nested bean @Java
public class SeaLandBean {

    @NotNull
    @Valid
    public List<PiariElement> piaris; // (NotNull, EmptyAllowed)

    @Required
    @Valid
    public List<BonvoElement> bonvos; // (NotNull, NotEmpty)

    public static class PiariElement {

        @Required
        public String dstore;
    }

    public static class BonvoElement {
        ...
    }
}

プロフェッショナルバリデーション

TODO jflute バリデーション内で検索したデータを使い回し、バリデーションエラー時にリダイレクト