onemuri.space

gin で入れ子の validation が効かない時

結論から

まず結論から話すと、 dive tag を使えば良いです。

入れこのときに validation が効かない?

golang で api 開発をしているときによく使われる web フレームワークとして Gin があります

gin は api の入り口/出口として router や request/response 、validation など(こんなもんじゃないけど)とても高機能なフレームワークです

ただ golang を書きはじめたときや、ふと忘れてしまう処理として、入れ子構造の場合の validation の設定方法です

具体的には以下のような構造体と validation が設定されていると想定します

type User struct {
  Name    string   `json:"name"`
  Friends []Friend `json:"friends"`
}

type Friend struct {
  Name  string `json:"name" binding:"required"`
  Phone string `json:"phone"`
}

上記のような構造体をリクエストとして受け取る API の場合、このような json が渡されたらどうなるでしょうか?

{
  "name": "hoge",
  "friends": [
    {
      "phone": "000-0000-0000"
    }
  ]
}

直感的に考えれば、 friends.name が json に書かれておらず、 Friend 構造体の Name field の binding に設定されている required tag が聞くために validation に引っかかりそうではないですか?

少なくとも私はそう考えて、api を開発したことがあります。テストを作る段階や、いろんなパラメータで試しにリクエストを叩いたときに、ふとおかしなことに気づきます。上記のような json を渡しても validation のエラーが吐かれていないのです。

gin と binding のサンプルコード

とても簡易的ではありますが、試しに以下のコードでサーバーを起動してリクエストを叩いてみます
失敗すれば、errorのキーを持つ json が返され、成功すれば、bind した構造体が返されます。

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/no_dive", noDive)
  r.Run()
}

type NoDiveUser struct {
  Name    string   `json:"name"`
  Friends []Friend `json:"friends"`
}

type Friend struct {
  Name  string `json:"name" binding:"required"`
  Phone string `json:"phone"`
}

func noDive(ctx *gin.Context) {
  u := &NoDiveUser{}
  if err := ctx.ShouldBindJSON(u); err != nil {
    ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, *u)
}

gin を使いはじめて初期の頃は、先ほどの構造体(User だけど、以下サンプルコードでは NoDiveUser )に対して、friends.name が設定されていないリクエストを叩いたとしたらエラーメッセージが返ってくると思っていました。

リクエストは curl でも postman でも何でも良いですが、一旦 curl で叩いてみます

curl --location --request GET 'localhost:8080/no_dive' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "hoge",
    "friends": [
        {
            "phone": "000-0000-0000"
        }
    ]
}' | jq

結果

{
  "name": "hoge",
  "friends": [
    {
      "name": "",
      "phone": "000-0000-0000"
    }
  ]
}

構造体が返ってきていますね…

ふむー。どうやら先ほどまでの理解は間違っていたようです

slice(入れ子)の場合には dive tag を使う

gin validate slice などでググっていると(というか公式を読めよという話ではある)、以下の記事を見つけました。

https://github.com/gin-gonic/gin/issues/1041

gin の issues に投稿されていました

その中で以下のコメントがありました

you have to use dive tag that tells the validator to dive into a slice, array or map.

なるほど。求めていた情報を見つけました。

gin で slice の validation をするサンプルコード

先ほどと異なるのは、User の構造体の Friends fields に binding:"dive" の tag をつけたことです

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/dive", dive)
  r.Run()
}

type User struct {
  Name    string   `json:"name"`
  Friends []Friend `json:"friends" binding:"dive"`
}

type Friend struct {
  Name  string `json:"name" binding:"required"`
  Phone string `json:"phone"`
}

func dive(ctx *gin.Context) {
  u := &User{}
  if err := ctx.ShouldBindJSON(u); err != nil {
    ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, *u)
}

さて、先ほどと同じ json request を叩いてみましょう

curl --location --request GET 'localhost:8080/dive' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "hoge",
    "friends": [
        {
            "phone": "000-0000-0000"
        }
    ]
}' | jq

結果

{
  "error": "Key: 'User.Friends[0].Name' Error:Field validation for 'Name' failed on the 'required' tag"
}

これです。求めていたやつです。

まとめ

gin の入れ子の validation は dive tag を使いましょう。

ちなみに、公式のドキュメントあります。

最初から公式を読みましょう!(と、言いつつググった方が早い時もあるよね)

自己紹介用画像

Riki Akagi

2019年からDeNAで働いています。GCP(CloudSQL・GAE・Cloud Function etc)とGoでAPI開発に勤んでいます。睡眠やエンジニアリングに関することに興味を持って過ごしているのでその情報を皆さんに共有していけたらなと思っています。

自己紹介の詳細