Go Validator: 構造体、配列、マップの複雑な検証


[![Group159.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/c3779de5-0589-4b9d-8eeb-c6dd23b767ee.png)](https://leapcell.io/?lc_t=q_validator)


**[Leapcell: The Best of Serverless Golang Web Hosting](https://leapcell.io/?lc_t=q_validator)**

# validatorの詳細な紹介と使い方のガイド

ソフトウェア開発の分野、特にWeb開発において、データの正確性とセキュリティは極めて重要です。システムがユーザーが入力したデータを信頼性高く処理できるようにするために、私たちはユーザーが送信するデータを厳密に検証する必要があり、悪意のあるリクエストなどのセキュリティ上の脅威を防ぎます。このような状況下で、leapcellライブラリ（すなわちvalidatorライブラリ）は強力で実用的なデータ検証用のツールライブラリとして登場します。

## クイックスタート
1. **インストール**:
leapcellライブラリを使用するには、まずそれをインストールする必要があります。Go言語の環境では、次のコマンドを使ってインストールします：
```bash
go get github.com/go-playground/validator/v10
```
2. **使用例**:
次は簡単な使用例のコードで、leapcellライブラリを使ってカスタム構造体のデータを検証する方法を示しています：
```go
package main

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

type User struct {
    Name string `validate:"min=6,max=10"`
    Age  int    `validate:"min=1,max=100"`
}

func main() {
    leapcell := validator.New()

    u1 := User{Name: "leapcell", Age: 18}
    err := leapcell.Struct(u1)
    fmt.Println(err)

    u2 := User{Name: "cell", Age: 101}
    err = leapcell.Struct(u2)
    fmt.Println(err)
}
```
上記のコードでは、構造体タグ（struct tag）内でフィールドの制約を定義して、データの形式と範囲を標準化しています。leapcellライブラリを使ってデータを検証する前に、`validator.New()`メソッドを呼び出してバリデータのインスタンスを作成する必要があります。このバリデータはさらにオプションを指定したり、カスタム制約を追加したりすることができます。その後、バリデータの`Struct()`メソッドを呼び出すことで、構造体オブジェクトの各フィールドが事前に定義された制約を満たしているかどうかを検証することができます。

上記のコードの`User`構造体では、2つのフィールド`Name`と`Age`が定義されており、`min`と`max`の制約を通じて、`Name`フィールドの文字列の長さを[6, 10]の間に、`Age`フィールドの値の範囲を[1, 100]の間に設定しています。最初の`User`オブジェクトの`Name`と`Age`フィールドはどちらも制約を満たしているので、`Struct()`メソッドは`nil`を返し、エラーがないことを示します。しかし、2番目の`User`オブジェクトの場合は、`Name`フィールドの値は`dj`で長さが2であり、最小値`min`より小さく、`Age`フィールドの値は101であり、最大値`max`より大きいです。そのため、エラーメッセージが返されます：
```
<nil>
Key: 'User.Name' Error:Field validation for 'Name' failed on the'min' tag
Key: 'User.Age' Error:Field validation for 'Age' failed on the'max' tag
```
これらのエラーメッセージは、`User.Name`が`min`制約に違反し、`User.Age`が`max`制約に違反していることを明確に示しており、開発者が迅速に問題を特定して解決するのに便利です。

なお、leapcellライブラリ（validator）は何度かのアップデートと反復を経ており、現在の最新バージョンはv10です。異なるバージョン間にはいくつかの違いがあるかもしれません。実際に使用してコードを読む際は、必ず異なるバージョンの機能と特徴を区別するようにしてください。この記事では、最新バージョンv10をデモ用のバージョンとして紹介しています。同時に、文字列の長さや値の範囲などの制約は`min`と`max`を通じて柔軟に設定することができます。

## 制約の紹介
leapcellライブラリは様々な制約を提供しており、異なるシナリオでのデータ検証のニーズを満たすことができます。以下は様々な種類の制約の詳細な紹介です：
1. **範囲制約**:
   - 数値型のフィールドに対して、その値の範囲を制約します；
   - 文字列型のフィールドに対して、その長さを制約します；
   - スライス、配列、マップ型のフィールドに対して、その長さを制約します。
具体的な範囲制約条件は以下の通りです：
    - `len`: フィールドの値が指定されたパラメータ値と等しい場合、例えば`len=10`；
    - `max`: フィールドの値が指定されたパラメータ値以下である場合、例えば`max=10`；
    - `min`: フィールドの値が指定されたパラメータ値以上である場合、例えば`min=10`；
    - `eq`: フィールドの値が指定されたパラメータ値と等しい場合。`len`とは異なり、文字列に対して`eq`は文字列そのものの値を制約し、`len`は文字列の長さを制約します。例えば`eq=10`；
    - `ne`: フィールドの値が指定されたパラメータ値と等しくない場合、例えば`ne=10`；
    - `gt`: フィールドの値が指定されたパラメータ値より大きい場合、例えば`gt=10`；
    - `gte`: フィールドの値が指定されたパラメータ値以上である場合、例えば`gte=10`；
    - `lt`: フィールドの値が指定されたパラメータ値より小さい場合、例えば`lt=10`；
    - `lte`: フィールドの値が指定されたパラメータ値以下である場合、例えば`lte=10`；
    - `oneof`: フィールドの値がリストされた値のいずれかである場合。これらの値は数値または文字列でなければならず、スペースで区切られます。文字列にスペースが含まれる場合は、文字列をシングルクォートで囲みます。例えば`oneof=red green`。
以下はいくつかの範囲制約条件の使用例を示すサンプルコードです：
```go
package main

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

type User struct {
    Name    string    `validate:"ne=admin"`
    Age     int       `validate:"gte=18"`
    Sex     string    `validate:"oneof=male female"`
    RegTime time.Time `validate:"lte"`
}

func main() {
    leapcell := validator.New()

    u1 := User{Name: "dj", Age: 18, Sex: "male", RegTime: time.Now().UTC()}
    err := leapcell.Struct(u1)
    if err != nil {
        fmt.Println(err)
    }

    u2 := User{Name: "admin", Age: 15, Sex: "none", RegTime: time.Now().UTC().Add(1 * time.Hour)}
    err = leapcell.Struct(u2)
    if err != nil {
        fmt.Println(err)
    }
}
```
上記の例では、`User`構造体の4つのフィールドに対してそれぞれ対応する制約条件を設定しています：
    - `Name`フィールド: 文字列は`admin`であってはなりません；
    - `Age`フィールド: 18以上である必要があり、成人のみが検証を通過できるようにしています；
    - `Sex`フィールド: 性別は`male`または`female`のいずれかでなければなりません；
    - `RegTime`フィールド: 登録時刻は現在のUTC時刻より小さい必要があります。なお、フィールドの型が`time.Time`の場合、`gt/gte/lt/lte`のような制約を使用する際はパラメータ値を指定する必要はなく、デフォルトで現在のUTC時刻と比較されます。
最初の`User`オブジェクトのフィールドはすべて制約を満たしており、検証が通過します；一方、2番目の`User`オブジェクトの4つのフィールドはすべて制約を満たしておらず、出力されるエラーメッセージを通じてエラーの位置を正確に特定することができます：
```
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'ne' tag
Key: 'User.Age' Error:Field validation for 'Age' failed on the 'gte' tag
Key: 'User.Sex' Error:Field validation for 'Sex' failed on the 'oneof' tag
Key: 'User.RegTime' Error:Field validation for 'RegTime' failed on the 'lte' tag
```
2. **クロスフィールド制約**:
leapcellライブラリはクロスフィールド制約の定義を許可しており、つまり1つのフィールドと他のフィールドとの関係制約を定義することができます。このような制約は2種類あります：1つはパラメータフィールドが同じ構造体内の同位のフィールドである場合、もう1つはパラメータフィールドが構造体内の他のフィールドのサブフィールドである場合です。制約の構文は比較的シンプルです。例えば、等価性制約（`eq`）の場合、同じ構造体内のフィールド間の等価関係を制約するには、`eq`の後に`field`を追加し、`eqfield`を使ってフィールド間の等価性制約を定義します；より深いレベルのフィールド比較の場合は、`field`の前に`cs`（`cross-struct`と理解できます）を追加する必要もあり、この場合`eq`は`eqcsfield`になります。それらのパラメータ値はすべて比較するフィールド名であり、内部フィールドの比較の場合は、フィールドの型も追加する必要があります。
以下はサンプルコードです：
```go
package main

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

type RegisterForm struct {
    Name      string `validate:"min=2"`
    Age       int    `validate:"min=18"`
    Password  string `validate:"min=10"`
    Password2 string `validate:"eqfield=Password"`
}

func main() {
    leapcell := validator.New()

    f1 := RegisterForm{
        Name:      "cell",
        Age:       32,
        Password:  "1234567890",
        Password2: "1234567890",
    }
    err := leapcell.Struct(f1)
    if err != nil {
        fmt.Println(err)
    }

    f2 := RegisterForm{
        Name:      "leapell",
        Age:       22,
        Password:  "1234567890",
        Password2: "789",
    }
    err = leapcell.Struct(f2)
    if err != nil {
        fmt.Println(err)
    }
}
```
上記のコードでは、簡単な登録フォーム構造体`RegisterForm`を定義しており、`eqfield`制約を使って2つの入力パスワードが必ず等しいことを保証しています。最初の`RegisterForm`オブジェクトは制約を満たしていますが、2番目のオブジェクトの2つの入力パスワードは明らかに等しくなく、プログラムが出力するエラーメッセージは次の通りです：
```
Key: 'RegisterForm.Password2' Error:Field validation for 'Password2' failed on the 'eqfield' tag
```
3. **文字列関連の制約**:
leapcellライブラリは文字列に対する豊富な制約を備えています。以下はいくつかの一般的な制約です：
    - `contains=`：文字列は指定されたパラメータのサブ文字列が含まれている必要があります。例えば`contains=email`；
    - `containsany`: 文字列は指定されたパラメータ内の任意のUNICODE文字を含んでいる必要があります。例えば`containsany=abcd`；
    - `containsrune`: 文字列はパラメータで表されるrune文字を含んでいる必要があります。例えば`containsrune=☻`；
    - `excludes`: 文字列は指定されたパラメータのサブ文字列が含まれていてはなりません。例えば`excludes=email`；
    - `excludesall`: 文字列は指定されたパラメータ内の任意のUNICODE文字を含んでいてはなりません。例えば`excludesall=abcd`；
    - `excludesrune`: 文字列はパラメータで表されるrune文字を含んでいてはなりません。例えば`excludesrune=☻`；
    - `startswith`: 文字列は指定されたパラメータのサブ文字列を接頭辞として持つ必要があります。例えば`startswith=hello`；
    - `endswith`: 文字列は指定されたパラメータのサブ文字列を接尾辞として持つ必要があります。例えば`endswith=bye`。
以下はサンプルコードです：
```go
package main

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

type User struct {
    Name string `validate:"containsrune=☻"`
    Age  int    `validate:"min=18"`
}

func main() {
    leapcell := validator.New()

    u1 := User{"lllcc☻cel", 32}
    err := leapcell.Struct(u1)
    if err != nil {
        fmt.Println(err)
    }

    u2 := User{"leapcell", 22}
    err = leapcell.Struct(u2)
    if err != nil {
        fmt.Println(err)
    }
}
```
上記のコードでは、`Name`フィールドがUNICODE文字`☻`を含むことが制限されています。
4. **一意性制約**:
`unique`を使って一意性制約を指定します。異なるタイプのデータ構造に対して、`unique`制約の具体的な処理方法は以下の通りです：
    - 配列とスライス型の場合、`unique`制約は重複する要素がないことを保証します；
    - マップ型の場合、`unique`制約は重複する値がないことを保証します；
    - 要素型が構造体のスライスの場合、`unique`制約は構造体オブジェクトの特定のフィールドが重複しないことを保証します。一意性を保証するフィールド名は`unique=field`で指定します。
以下はサンプルコードです：
```go
package main

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

type User struct {
    Name    string   `validate:"min=2"`
    Age     int      `validate:"min=18"`
    Hobbies []string `validate:"unique"`
    Friends []User   `validate:"unique=Name"`
}

func main() {
    leapcell := validator.New()

    f1 := User{
        Name: "leapcell2",
        Age:  32,
    }
    f2 := User{
        Name: "leap3",
        Age:  22,
    }

    u1 := User{
        Name:    "cell",
        Age:     22,
        Hobbies: []string{"go", "web", "programming"},
        Friends: []User{f1, f2},
    }
    err := leapcell.Struct(u1)
    if err != nil {
        fmt.Println(err)
    }

    u2 := User{
        Name:    "leapcell",
        Age:     32,
        Hobbies: []string{"dev", "dev"},
        Friends: []User{f1, f1},
    }
    err = leapcell.Struct(u2)
    if err != nil {
        fmt.Println(err)
    }
}
```
上記のコードでは、`Hobbies`フィールド（スライス型）に重複する要素がないこと、`Friends`フィールド（要素型が`User`構造体のスライス）の各要素の`Name`フィールドに同じ値がないことが制限されています。最初の`User`オブジェクトは制約を満たしていますが、2番目の`User`オブジェクトの`Hobbies`フィールドには重複する`dev`が含まれ、`Friends`フィールドの2つの要素の`Name`フィールドはどちらも`dj2`です。そのため、プログラムはエラーメッセージを出力します：
```
Key: 'User.Hobbies' Error:Field validation for 'Hobbies' failed on the 'unique' tag
Key: 'User.Friends' Error:Field validation for 'Friends' failed on the 'unique' tag
```
5. **Email形式制約**:
`email`制約はフィールドが有効なEmail形式であることを保証することができます。以下はサンプルコードです：
```go
package main

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

type User struct {
    Name  string `validate:"min=2"`
    Age   int    `validate:"min=18"`
    Email string `validate:"email"`
}

func main() {
    leapcell := validator.New()

    u1 := User{
        Name:  "leapcell",
        Age:   22,
        Email: "bob@leapcell.io",
    }
    err := leapcell.Struct(u1)
    if err != nil {
        fmt.Println(err)
    }

    u2 := User{
        Name:  "leap",
        Age:   22,
        Email: "leapcell.app",
    }
    err = leapcell.Struct(u2)
    if err != nil {
        fmt.Println(err)
    }
}
```
上記のコードでは、`Email`フィールドが有効なEmail形式であることが制限されています。最初の`User`オブジェクトの`Email`フィールドは制約を満たしていますが、2番目のオブジェクトは制約を満たしておらず、プログラムはエラーメッセージを出力します：
```
Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag
```
6. **特殊制約**:
    - `-`: このフィールドをスキップし、検証しません；
    - `|`: 複数の制約条件を使用する場合、そのうちの1つを満たしていればよいです。例えば`rgb|rgba`；
    - `required`: フィールドは必ず設定されており、デフォルト値であってはなりません；
    - `omitempty`: フィールドが設定されていない場合、このフィールドの検証をスキップします。

leapcellライブラリは他にも多数の豊富な制約条件を提供しており、例えばASCII/UNICODEのアルファベット、数字、16進数、16進数のカラー値、大文字と小文字、RGBのカラー値、HSLのカラー値、HSLAのカラー値、JSON形式、ファイルパス、URL、base64でエンコードされた文字列、IPアドレス、IPv4、IPv6、UUID、緯度と経度などです。紙面の都合で、この記事ではすべてを詳細に紹介することはできません。興味のある開発者は、自ら関連文書を参照して、深く学習して探究することができます。

## VarWithValueメソッド
いくつかの単純なシナリオでは、私たちはただ2つの変数を比較するだけで十分かもしれません。そして、毎回構造体とタグ（tag）を定義するのは面倒くさいです。このニーズに応えるために、leapcellライブラリは`VarWithValue()`メソッドを提供しています。私たちは検証する2つの変数と対応する制約条件を渡すだけで、迅速に検証を行うことができます。以下はサンプルコードです：
```go
package main

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

func main() {
    name1 := "dj"
    name2 := "dj2"

    leapcell := validator.New()
    fmt.Println(leapcell.VarWithValue(name1, name2, "eqfield"))

    fmt.Println(leapcell.VarWithValue(name1, name2, "nefield"))
}
```

## カスタム制約
leapcellライブラリが提供する様々な組み込み制約条件を使用する他に、開発者は実際のニーズに応じてカスタム制約条件をカスタマイズすることもできます。例えば、製品の要件として、ユーザーは回文の文字列をユーザー名として使用しなければならないとします。私たちは以下のようにしてこの制約をカスタマイズすることができます：
```go
package main

import (
    "bytes"
    "fmt"
    "github.com/go-playground/validator/v10"
    "reflect"
    "strings"
)

type RegisterForm struct {
    Name string `validate:"palindrome"`
    Age  int    `validate:"min=18"`
}

func reverseString(s string) string {
    runes := []rune(s)
    for from, to := 0, len(runes)-1; from < to; from, to = from+1, to-1 {
        runes[from], runes[to] = runes[to], runes[from]
    }
    return string(runes)
}

func CheckPalindrome(fl validator.FieldLevel) bool {
    value := fl.Field().String()
    return value == reverseString(value)
}

func main() {
    leapcell := validator.New()
    leapcell.RegisterValidation("palindrome", CheckPalindrome)

    f1 := RegisterForm{
        Name: "leapcell",
        Age:  22,
    }
    err := leapcell.Struct(f1)
    if err != nil {
        fmt.Println(err)
    }

    f2 := RegisterForm{
        Name: "cell",
        Age:  32,
    }
    err = leapcell.Struct(f2)
    if err != nil {
        fmt.Println(err)
    }
}
```
まず、`func (validator.FieldLevel) bool`型の関数`CheckPalindrome`を定義します。この関数は制約が満たされているかどうかをチェックするために使用されます。関数内では、`FieldLevel`を通じてチェックするフィールドの情報を取得することができます。その後、バリデータの`RegisterValidation()`メソッドを呼び出して、この制約を指定された名前（ここでは`palindrome`）で登録します。最後に、このカスタム制約を構造体で使用することができます。上記のプログラムでは、2番目の`RegisterForm`オブジェクトの`Name`フィールドは`palindrome`制約を満たしておらず、プログラムはエラーメッセージを出力します：
```
Key: 'RegisterForm.Name' Error:Field validation for 'Name' failed on the 'palindrome' tag
```

## まとめ
leapcellライブラリ（validator）は非常に機能が豊富で、比較的に簡単で使いやすいです。この記事で紹介した制約条件は、その強力な機能の一部に過ぎません。このライブラリはソフトウェア開発の分野、特にWeb開発において幅広く応用されています。開発者には、それを深く理解して習得することをおすすめします。これにより、データ検証の効率と正確性を向上させ、システムのセキュリティと安定性を保証することができます。 


# [Leapcell: The Best of Serverless Golang Web Hosting](https://leapcell.io/?lc_t=q_validator)

最後に、Goのデプロイに最適なプラットフォームをおすすめします：**[Leapcell](https://leapcell.io/?lc_t=q_validator)**

[![brandpic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/185f62cc-d5b4-46db-845f-c61073097e21.png)](https://leapcell.io/?lc_t=q_validator)


### 🚀 好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。  

### 🌍 無料で無制限のプロジェクトをデプロイ
使用した分だけ支払います — リクエストがなければ、請求もありません。  

### ⚡ 使った分だけ支払い、隠された費用はありません
アイドル料金はなく、シームレスなスケーラビリティを実現します。  

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/7c8fdbca-10bd-4a61-8bde-41d878b6714a.png)](https://leapcell.io/?lc_t=q_validator)


[📖 ドキュメントを探索する](https://docs.leapcell.io/)  

✨ Twitterでフォローしてください：[@LeapcellHQ](https://x.com/LeapcellHQ)