Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

go-playground/validator リクエストパラメータ向けValidationパターンまとめ

概要

go-playground/validatorを用いたバリーデーションパターンをREST APIのリクエストパラメータでよく使われそうなケース別にまとめてみたのでご参考ください。
validatorはプリミティブな変数単位でも使用できますがここではリクエストパラメータを想定した構造体をメインに書いています。

go-playground/validatorとは

https://github.com/go-playground/validator
バリデーションに特化したgolangのOSS。
バリデーション対象の構造体にvalidateタグを付けてちょっとしたDSLっぽくバリデーション内容が書けるのが特徴的。
チェックできる内容は必須パラメータの有無、数値、文字数の範囲、任意のフォーマット等々。
validateで定義されている予約語についてGoDocを参照
自分でバリデーション用の予約語を定義して自作することもできる。

バージョン

go-playground/validator.v9

目次

  1. 使い方
  2. 代表的なバリデーションパターン
    1. 必須パラメータチェック
    2. 文字数チェック
    3. 禁止文字列チェック
    4. 含有文字列チェック
    5. 値範囲チェック
    6. 日付(yyyy-MM-dd形式)チェック
    7. 日時(RFC3339形式)チェック
    8. 集合チェック
    9. リストや入れ子構造体のチェック
    10. リストの重複チェック
  3. 使用例
  4. 終わりに

使い方

インストール

go get gopkg.in/go-playground/validator.v9

予約語を使用する場合

1.バリデーション対象の構造体にvalidateタグを付与する。
※バリーデーションの項目はカンマ区切りで複数選択可能

type User struct { 
   FirstName      string     `validate:"required"` //必須パラメータ
   LastName       string     `validate:"required"` //必須パラメータ
   Age            uint8      `validate:"gte=0,lt=130"` // 0以上、130未満
   Email          string     `validate:"required,email"` //必須パラメータ、かつ、emailフォーマット
}

2.データをセットし、バリデーションする

//バリデーション対象のデータをセット
user := &User{
   FirstName:      "Badger",
   LastName:       "Smith",
   Age:            135,
   Email:          "Badger.Smithgmail.com",
}
validate := validator.New()  //インスタンス生成
errors := validate.Struct(user) //バリデーションを実行し、NGの場合、ここでエラーが返る。

上記の例だと以下のようなエラー内容が返る

Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag

バリデーション内容を自作する場合

1.バリデーション対象の構造体に自作対象のバリデーション名を付与したタグを定義する。

type User struct { 
    BirthPlace     string     `validate:"is_japan,required"` //is_japanが自作バリデーション名とする
}

2.バリデーション内容を登録する

func main() {
    user := User{
        BirthPlace:"America",//"Japan"ではないのでエラーとなる
    }
    validate := validator.New()
    validate.RegisterValidation("is_japan", isJapan) //第一引数をvalidateタグで設定した名前に合わせる
    err := validate.Struct(user)
}
func isJapan(fl validator.FieldLevel) bool {  //引数の型、返り値は固定
    birthPlace := fl.Field().String()
    if birthPlace == "Japan" {
        return true
    }
    return false
}

上記の例だと以下のようなエラー内容が返る

Key: 'User.BirthPlace' Error:Field validation for 'BirthPlace' failed on the 'is_japan' tag

エラーメッセージを捕捉する方法

validatorで出されるエラーメッセージではNGとなったタグ名しかわからないため、以下のようにエラーとなった変数名やタグ名を捕捉して、独自のエラーメッセージに書き換えることもできる。

validate := validator.New()
err := validate.Struct(user)

var errorMessages []string  //バリデーションでNGとなった独自エラーメッセージを格納

if err != nil {
  for _, err := range err.(validator.ValidationErrors) {

    var errorMessage string
    fieldName := err.Field() //バリデーションでNGになった変数名を取得

    switch fieldName {
    case "Name":
      errorMessage = "error message for Name"
    case "Age":
      errorMessage = "error message for Age"
    case "Email":
      var typ = err.Tag() //バリデーションでNGになったタグ名を取得
      switch typ {
      case "required":
        errorMessage = "error message for required of Email"
      case "email":
        errorMessage = "error message for email format of Email"
      }
    }
    errorMessages = append(errorMessages, errorMessage)
  }
}

err.Field()の代わりにerr.Namespace()を使用するとフルの変数名(User.Name,User.Age等)が取得できる

補足

複雑なバリデーション内容は自作で書いたり、予約語のバリデーション内容と組み合わせたりできる。
ただし、バリデーションでのNG項目は一つのみであり、順序は設定できない。

type Sample struct{
  param string `gte=1,lte=200,isJapanese` //日本語で、かつ、1文字以上、200文字以内 ,isJapaneseは自作
}

例えば、上記のバリデーションで300文字の英文がparamに格納されていた場合、lte,isJapaneseのどちらも満たさないが、どちらが優先的にバリデーションでNGになるかは制御できない。
この辺は自作バリデーションメソッドに複数のチェック項目を含めたりして、制御する必要がありそう。

代表的なバリデーションパターン

リクエストパラメータのチェックに含まれそうな代表的なケースをいくつかピックアップした。
各例はバリデーション対象の構造体の要素を想定している。

必須パラメータチェック

paramA string `validate:"required"`
paramB int    `validate:"required"`

文字列の場合は空文字、数値の場合は0もrequiredでNGとなる

文字数チェック

paramA string `validate:"len=5"`         //5文字固定
paramB string `validate:"gt=0,lt=100"`   //1文字以上、100文字未満
paramC string `validate:"min=1,max=100"` //1文字以上、100文字以下

※lenはintパラメータに対する桁数では使えないの注意

禁止文字列チェック

paramA string `validate:"excludesall=!()#@{}"`//!()#@{}のいずれか一つでも含む文字列はNG

1文字の場合はexcludesが使える

含有文字列チェック

paramA string `validate:"containsany=!()#@{}"`//!()#@{}の少なくとも一つを含まない文字列はNG

1文字の場合はcontainsが使える

値範囲チェック

paramB int `validate:"gte=0,lt=100"`    //0以上100未満
paramC int `validate:"min=1,max=100"`   //1以上100以下

日付(yyyy-MM-dd形式)チェック

バリデーション用の予約語が存在しないため、以下を自作バリデーションメソッドとする。

func DateValidation(fl validator.FieldLevel) bool {
    _, err := time.Parse("2006-01-02", fl.Field().String())
    if err != nil {
        return false
    }
    return true
}

日時(RFC3339形式)チェック

バリデーション用の予約語が存在しないため、以下を自作バリデーションメソッドとする。
例)2018-11-11T23:20:52Z、2018-04-21T23:20:00.000Z等

func DatetimeValidation(fl validator.FieldLevel) bool {
    _, err := time.Parse(time.RFC3339, fl.Field().String())
    if err != nil {
        return false
    }
    return true
}

集合チェック

color string `validate:"oneof=red blue green"` //red,blue,greenのいずれかの文字列
typ   int    `validate:"oneof=1 3 5"` //1,3,5のいずれかの数値

※スペース区切りなので要注意

リストや入れ子構造体のチェック

diveがリストの中身をバリデーションする宣言。

type User struct{
   Addresses []Address `validate:"dive,required"` //Addressオブジェクトの中身をバリデーションする
   IDList    []int     `validate:"min=1,max=5,dive,min=0,max=100"` //要素数が1~5の0〜100までの値を持つリストのみ許容
}

type Address struct{
   City  string 
   Phone string `validate:"required"`
}

※diveの記述する位置によってmin,gt等の意味合いが変わってくるので注意。
詳細はGoDocを参照

リストの重複チェック

UniqueID []string `validate:"unique"` //リストの要素に重複値がある時はNG

使用例

ここまで挙げたようなバリデーションを実際に組み込んで見ると以下のように書くことができます。
UserValidateメソッドはUser構造体をリクエストパラメータとして受け取って、バリデーションのエラーメッセージのリストを返します。

sample.go
package sample

import (
    "github.com/go-playground/validator"
    "time"
    "fmt"
)

const (
    Name               = "Name"
    Sex                = "Sex"
    Age                = "Age"
    Email              = "Email"
    FavoriteColor      = "FavoriteColor"
    Addresses          = "Addresses"
    BirthDay           = "BirthDay"
    RegisteredDatetime = "RegisteredDatetime"
)

const (
    Date      = "date"
    DateRange = "date_range"
)

type User struct {
    Name               string    `validate:"required,min=5,max=10,excludesall=!()#@{}"`
    Sex                string    `validate:"len=1"`
    Age                int       `validate:"gt=0,lte=150"`
    Email              string    `validate:"email"`
    FavoriteColor      string    `validate:"oneof=red blue green"`
    Addresses          []Address `validate:"dive,required"`
    BirthDay           string    `validate:"date,date_range"`
    RegisteredDatetime string    `validate:"datetime"`
}

type Address struct {
    City  string `json:"city"`
    Phone string `json:"phone" validate:"required"`
}

func UserValidate(user *User) []string {

    var errorMessages []string

    validate := validator.New()

    validate.RegisterValidation("date", dateValidation)
    validate.RegisterValidation("datetime", datetimeValidation)
    validate.RegisterValidation("date_range", dateRangeValidation)

    err := validate.Struct(user)

    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {

            var errorMessage string
            fieldName := err.Field()

            switch fieldName {
            case Name:
                errorMessage = "error message for Name"
            case Sex:
                errorMessage = "error message for Sex"
            case Age:
                errorMessage = "error message for Age"
            case Email:
                errorMessage = "error message for Email"
            case FavoriteColor:
                errorMessage = "error message for FavoriteColor"
            case Addresses:
                errorMessage = "error message for Addresses"
            case BirthDay:
                errorTag := err.Tag()
                switch errorTag {
                case Date:
                    errorMessage = "error message for date format of Birthday"
                case DateRange:
                    errorMessage = "error message for date range of BirthDay"
                default:
                    errorMessage = "error message for BirthDay"
                }
            case RegisteredDatetime:
                errorMessage = "error message for RegisteredDatetime"
            default:
                errorMessage = "error message"
            }
            errorMessages = append(errorMessages, errorMessage)
        }
    }
    return errorMessages
}

func dateValidation(fl validator.FieldLevel) bool {

    _, err := time.Parse("2006-01-02", fl.Field().String())
    if err != nil {
        return false
    }
    return true
}

func datetimeValidation(fl validator.FieldLevel) bool {

    _, err := time.Parse(time.RFC3339, fl.Field().String())
    fmt.Println(err)
    if err != nil {
        return false
    }
    return true
}

func dateRangeValidation(fl validator.FieldLevel) bool {

    var date = fl.Field().String()
    var minDate = time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
    var maxDate = time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)

    datetime, err := time.Parse("2006-01-02", date)
    if err != nil {
        return false
    }
    if datetime.Before(minDate) || datetime.After(maxDate) {
        return false
    }
    return true
}


終わりに

validatorを用いたバリデーションパターンをいくつか紹介しました。
リクエストパラメータ用の構造体を見るだけで、どのような値を想定しているかを確認できるのはvalidatorの良いところだと思います。
他にもhostnameやIPAddressなど細かいフォーマットをチェックする予約語もいくつか含まれているので必要に応じてGoDocを確認したり、ないものは自作するのが良いかと思います。

RunEagler
都内でフリーランスエンジニアをしています。フルスタックの何でも屋さんです。 できる限り品質の良いものをお届けしますのでご参考にして頂ければ幸いです<(_ _)> 興味:Typescript/Go/Python/React/Vue/機械学習
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away