Ginフレームワークのバインディングとバリデーションを試してみる

# はじめに

Go言語+GinフレームワークでAPIを作成するにあたり、リクエストの形式がJSONのときに構造体へのバインドとバリデーションチェックを行う仕組みを試してみます。

https://gin-gonic.com/ja/docs/examples/binding-and-validation/

# 実行条件

```shell
Golang v1.22.3
Gin v1.10.0
air v1.52.0

```

# ディレクトリ構成

```shell
.
├── .air.toml
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
└── tmp
```

# 検証

`main.go`の中身はこんな感じ

```golang:main.go
package main

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

type JsonRequest struct {
    Id int `binding:"required,gte=0,lt=10"`         // 必須 0以上 10未満
    Ids []int `binding:"required,dive,gt=3,lte=8"`  // 必須 sliceのバリデーション 3超過 8以下
    Name string `binding:"min=0,max=5"`             // 0文字以上 5文字以下
    Names []string
    Flg bool
    Flgs []bool `binding:"required"`                // 必須
    Data Data `json:"data"`
    Datas []Data `binding:"required,dive"`          // 必須 sliceのバリデーション
}

type Data struct {
    IntParam int `json:"int_param"`
    StrParam string `json:"str_param" binding:"required"`      // 必須
    BoolParam bool `json:"bool_param"`
    ChildDatas []ChildData `json:"child_datas" binding:"dive"` //sliceのバリデーション
}

type ChildData struct {
    IntChild int `json:"int_child" binding:"gt=3"` // 3超過
    StrChild string `json:"str_child"`
    BoolChild bool `json:"bool_child"`
}

func main() {
    engine:= gin.Default()
    engine.POST("/checkjson", func(c *gin.Context) {
        var json JsonRequest

        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error" : err.Error(),
            })
            return
        }

        c.JSON(http.StatusOK, gin.H{
            "json" : json,
        })

    })

    engine.Run(":8080")
}
```

リクエストは次のような形を想定しています。

`POST http://localhost:8080/checkjson`


```json:リクエスト
{
    "id" : 9,
    "ids" : [4, 8],
    "name" : "12345",
    "names": ["aaaa", "bbbb"],
    "flg" : true,
    "flgs": [true, false],
    "data": {
        "int_param" : 1,
        "str_param" : "cccc",
        "bool_param" : true
    },
    "datas": [
        {
        "int_param" : 2,
        "str_param" : "dddd",
        "bool_param" : true,
        "child_datas" : [
            {
                "int_child" : 4,
                "str_child" : "ffff",
                "bool_child" : true
            }
        ]
        },
        {
        "int_param" : 3,
        "str_param" : "eeee",
        "bool_param" : false
        }
    ]
}
```

#### バインド

`main.go`の中身を見ていきましょう。
処理内容とは前後しますが、まずはバインド部分から。

```golang
        var json JsonRequest

        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{
                "error" : err.Error(),
            })
            return
        }
```

ginフレームワークのShouldBindJSONを利用してリクエストのjsonを構造体にバインドしています。

バインドにはMost BindとShuld Bindの2種類が利用できます。エラーを独自に制御したい場合は後者を利用しましょう。

:::note warn
Must bind

もしバインド時にエラーがあった場合、ユーザーからのリクエストは c.AbortWithError(400, err).SetType(ErrorTypeBind) で中止されます。この処理は、ステータスコード 400 を設定し、Content-Type ヘッダーに text/plain; charset=utf-8 をセットします。もしこのあとにステータスコードを設定しようとした場合、[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422 という注意メッセージが表示されるので注意してください。
:::

:::note warn
Should bind

もしバインド時にエラーがあった場合、エラーが返ってくるので、開発者の責任で、適切にエラーやリクエストをハンドリングします。
:::


バインドする際の構造体`JsonRequest`は次のとおりです。

```golang
type JsonRequest struct {
    Id int `binding:"required,gte=0,lt=10"`         // 必須 0以上 10未満
    Ids []int `binding:"required,dive,gt=3,lte=8"`  // 必須 sliceのバリデーション 3超過 8以下
    Name string `binding:"min=0,max=5"`             // 0文字以上 5文字以下
    Names []string
    Flg bool
    Flgs []bool `binding:"required"`                // 必須
    Data Data `json:"data"`
    Datas []Data `binding:"required,dive"`          // 必須 sliceのバリデーション
}

type Data struct {
    IntParam int `json:"int_param"`
    StrParam string `json:"str_param" binding:"required"`      // 必須
    BoolParam bool `json:"bool_param"`
    ChildDatas []ChildData `json:"child_datas" binding:"dive"` //sliceのバリデーション
}

type ChildData struct {
    IntChild int `json:"int_child" binding:"gt=3"` // 3超過
    StrChild string `json:"str_child"`
    BoolChild bool `json:"bool_child"`
}
```

いろいろと試してみたかったので、配列やネストも用意しています。
キー`data`や`child_data`はネストしているため、さらに構造体を作成(Data, ChildData)しています。


バインドはjsonのキー名と構造体が持つキー名で結び付けられますが、タグをつけて任意で結び付けることもできます。
``` IntParam int `json:"int_param"` ```のように、jsonリクエストではint_paramというキーの値をIntParamにバインドします。

#### バリデーション

jsonのバリデーションを見ていきましょう。

バリデーションは構造体から取り出した値に対して行うこともできますが、簡単な判定であればタグをつけて行うこともできます。

使い方は``` `binding:バリデーション条件` ```です。

例えば``` Id int `binding:"required,gte=0,lt=10"` ``` とした場合,
idは次のバリデーションが適用されます。
- 必須(=required) ※配列の場合はキーの存在チェック
- idが0以上(=gte = greater than or equal)
- 10未満(=lt = less than)

利用可能なバリデーションは以下に記載があります。

https://github.com/go-playground/validator


jsonのバインドとバリデーションを両方行う場合は次のようになります。

``` `json:"str_param" binding:"required"` ```

ここでのハマりポイントは、 **jsonの指定とbindingの指定の間はカンマやセミコロンで区切ってはいけない（半角スペース区切りが正）** ということです。
（筆者は慣れていないためにこのミスを何度かやりました…エラーが出ず、バリデーションチェックもされない…）

:::note warn
正： ``` `json:"str_param" binding:"required"` ```

誤： ``` `json:"str_param";binding:"required"` ``` // ;は誤り
誤： ``` `json:"str_param",binding:"required"` ``` // ,は誤り
:::

もう少し見ていきましょう。

次のハマりポイントはsliceした値のバリデーションです。

例えば`Ids []int` に必須以外のバリデーションを行いたい場合、次の書き方ではうまくいきません。

``` Ids []int `binding:"required,gt=3,lte=8"` ```

sliceの必須チェック（キーの存在チェック）を行うだけであれば``` Ids []int `binding:"required"` ```の書き方で問題ないのですが、
sliceの値のバリデーションを行いたい場合には`dive`を指定しなければなりません。

``` Ids []int `binding:"required,dive,gt=3,lte=8"` ```

この場合、jsonにキー`ids`が存在しているかを判定し、idsの各値が3より大きく8以下であることを検証できます。

:::note warn
次のようにdiveをrequiredの前に書いた場合はどうなるでしょうか？

``` Ids []int `binding:"dive,required,gt=3,lte=8"` ```

この場合、必須チェック(required)が無視されます。
理由はまだよくわかりません…。難しい。
:::

なお、以下に`dive`に関するリファレンスがありますが、Go言語の初学者には厳しいって…。

https://pkg.go.dev/gopkg.in/go-playground/validator.v9#hdr-Dive


:::note warn
複数のバリデーションを指定する際、見づらいからといってbindingの値にスペースを入れてはいけません。
ビルドエラーが出ず、リクエスト時にエラーが発生します。

正：``` Ids []int `binding:"required,dive,gt=3,lte=8"` ```
誤：``` Ids []int `binding:"required, dive, gt=3, lte=8"` ```
:::

# まとめ
いかがだったでしょうか。

便利なようで便利でないような…実装バグが起こっているのかそうでないのか、わかりづらさを感じてしまいますね。相当細かな試験が必要そう。

ひとつでもモデルケースを作れれば強固なシステムが作れそうですが、その道のりが遠そうですね。
（たとえばバリデーションエラーの場合に内容を加工してjsonで返却する場合にはどうしたらいいのかとか）

それではまた。