Gin基礎 #3 リクエストのバインディングとバリデーション
前回の記事ではルーティングを扱いました。今回の記事は、入ってくるリクエストデータをGoの構造体で受け取るバインディングと、その値が正しいか確認するバリデーションを整理します。POST/PUTリクエストをきちんと処理するうえで、必ず押さえておきたい内容です。
- #1 はじめてのサーバー
- #2 ルーティングとハンドラ
- #3 リクエストのバインディングとバリデーション ← 今回の記事
- #4 レスポンス処理 — JSON、ステータスコード、エラー
- #5 ミドルウェア
- #6 データベース連携 (GORM)
- #7 プロジェクト構成とミニREST API
バインディングとは? #
クライアントが送ってきたJSONボディやフォームデータを、あらかじめ定義したGoの構造体に詰め込む作業をバインディングと呼びます。フィールドを1つずつ取り出して変換する代わりに、構造体1つで一度に受け取ります。
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}json:"name" タグは、JSONのどのキーをどのフィールドにマッピングするかを指定します。JSONキーが name なら Name フィールドに入ります。
JSONボディのバインディング — ShouldBindJSON #
c.ShouldBindJSON がリクエストボディを読んで構造体に詰めます。
func createUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"received": req})
}curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"カーティス","email":"a@b.com","age":30}'
# {"received":{"name":"カーティス","email":"a@b.com","age":30}}構造体のポインタを渡している点に注目してください。バインディングは構造体の値を埋めなければならないので、&req でアドレスを渡します。ボディの形式が間違っていたり型が合わなかったりするとエラーが返り、上のコードは400で応答してから return で処理を終えます。
バリデーション — bindingタグ #
バインディングだけでは「値が入ってきたか」「形式が合っているか」までは確認できません。Ginは構造体タグでバリデーションルールを宣言する機能を内蔵しています。binding タグにルールを書きます。
type CreateUserRequest struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"gte=0,lte=130"`
}required— 値が必ず存在しなければならないemail— メールアドレスの形式でなければならないgte=0,lte=130— 0以上130以下
これでルールに反するリクエストは ShouldBindJSON の段階でエラーになります。
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"","email":"not-an-email","age":200}'
# 400、バリデーションエラーメッセージよく使うルールをもう少し整理します。
| タグ | 意味 |
|---|---|
required | 値が空であってはならない |
email | メールアドレス形式 |
min=3 / max=20 | 文字列の長さまたは数値の大きさの範囲 |
gte=0 / lte=100 | 以上 / 以下 |
oneof=admin user | 列挙した値のいずれか |
len=10 | 正確な長さ |
バリデーションエンジンは go-playground/validator で、さらに多くのルールはそのドキュメントで確認できます。
ShouldBind vs Bind #
Ginのバインディング関数は2つの系統に分かれます。
ShouldBindJSON系 — エラーを返すだけです。レスポンスは自分で書きます。BindJSON系 — エラーが出ると自動的に400とともにリクエストを中断(abort)します。
// エラーを自分で処理 — 推奨
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 自動400 — レスポンス形式を制御しにくい
if err := c.BindJSON(&req); err != nil {
return // Ginがすでに400レスポンスを送っている
}APIのエラーレスポンス形式を一貫して制御したいなら、ShouldBind 系を使うほうがよいです。このシリーズは ShouldBind を基本に使います。
クエリとパスもバインディング #
ボディだけでなく、クエリストリングやパスパラメータも構造体で受け取れます。パラメータが多いときにハンドラがすっきりします。
type ListQuery struct {
Page int `form:"page,default=1" binding:"gte=1"`
Size int `form:"size,default=20" binding:"gte=1,lte=100"`
Sort string `form:"sort"`
}
func listUsers(c *gin.Context) {
var q ListQuery
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"query": q})
}クエリは form タグでキーを指定し、default= で初期値を与えられます。パスパラメータは uri タグと c.ShouldBindUri を使います。
type UserURI struct {
ID int `uri:"id" binding:"required"`
}
func getUser(c *gin.Context) {
var u UserURI
if err := c.ShouldBindUri(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"userID": u.ID})
}フォームデータのバインディング #
application/x-www-form-urlencoded や multipart/form-data のフォームも同じ方法で受け取ります。form タグを使って c.ShouldBind を呼べば、GinがContent-Typeを見て適切に処理します。
type LoginForm struct {
Username string `form:"username" binding:"required"`
Password string `form:"password" binding:"required"`
}
func login(c *gin.Context) {
var form LoginForm
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"user": form.Username})
}バリデーションエラーを整えて応答する #
err.Error() をそのまま出すと、メッセージが冗長でユーザーには読みにくいです。どのフィールドがなぜ失敗したかを整理して返すと、クライアントが処理しやすくなります。
import "github.com/go-playground/validator/v10"
if err := c.ShouldBindJSON(&req); err != nil {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
out := make(map[string]string, len(ve))
for _, fe := range ve {
out[fe.Field()] = fe.Tag() // 例: "Email": "email"
}
c.JSON(http.StatusBadRequest, gin.H{"errors": out})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "不正なリクエスト"})
return
}エラーメッセージをさらに精密に整える作業は、中級シリーズのエラー処理編で続けます。
まとめ #
今回の記事で整理した内容です。
- バインディングはリクエストデータをGoの構造体に詰める作業、
json/form/uriタグでマッピング - ボディは
c.ShouldBindJSON、クエリはc.ShouldBindQuery、パスはc.ShouldBindUri - 構造体のポインタ(
&req)を渡してこそ値が埋まる bindingタグでバリデーションルールを宣言、required/email/gte/oneofなどShouldBind系はエラーだけ返し、Bind系は自動400、制御力はShouldBindのほうが大きい- バリデーションエラーは
validator.ValidationErrorsでフィールドごとに整理して応答できる
次の記事(#4 レスポンス処理)では、JSON以外の多様なレスポンス形式とステータスコード、そして一貫したエラーレスポンスの作り方を整理します。