はじめに
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
の中身はこんな感じ
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"`
}
いろいろと試してみたかったので、配列やネストも用意しています。
キー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)
利用可能なバリデーションは以下に記載があります。
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で返却する場合にはどうしたらいいのかとか)
それではまた。