0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

実行条件

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

ディレクトリ構成

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

検証

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

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

リクエスト
{
    "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の中身を見ていきましょう。
処理内容とは前後しますが、まずはバインド部分から。

        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種類が利用できます。エラーを独自に制御したい場合は後者を利用しましょう。

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 という注意メッセージが表示されるので注意してください。

Should bind

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

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

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"`
}

いろいろと試してみたかったので、配列やネストも用意しています。
キーdatachild_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)

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

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

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

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

正: `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以下であることを検証できます。

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

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

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

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

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

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

まとめ

いかがだったでしょうか。

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

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

それではまた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?