前のセクションでは、リクエストパラメータを直接読み取る方法を紹介しました。個々のパラメータを個別に読み取るのが面倒な場合、irisフレームワークには、リクエストパラメータを構造体にバインドするパラメータバインディングメカニズムが備わっており、またフォームパラメータの検証メカニズムもサポートしています。

モデルバインディングとバリデーション

リクエストボディを型にバインドするには、モデルバインディングを使用します。現在、JSONJSONProtobufProtobufMsgPackXMLYAML、および標準フォーム値(foo=bar&boo=baz)などの型のバインディングをサポートしています。

// 以下はさまざまなフォーマットのリクエストパラメータをstructにバインドするための関数定義です
ReadJSON(outPtr interface{}) error
ReadJSONProtobuf(ptr proto.Message, opts ...ProtoUnmarshalOptions) error
ReadProtobuf(ptr proto.Message) error
ReadMsgPack(ptr interface{}) error
ReadXML(outPtr interface{}) error
ReadYAML(outPtr interface{}) error
ReadForm(formObject interface{}) error
ReadQuery(ptr interface{}) error

ReadBodyを使用すると、IrisはContent-Typeヘッダーに基づいてバインダーを推測します。バインドしたいコンテンツが確実な場合は、ReadXXXメソッド(例:ReadJSONReadProtobuf)を使用できます。

ReadBody(ptr interface{}) error

Irisには、賢明な組み込みデータバリデーションが付属しています。ただし、ReadJSONReadXMLなどのメソッドに自動的に呼び出されるバリデータをアタッチすることができます。この例では、go-playground/validator/v10を使用してリクエストボディをバリデートする方法について学びます。

すべてのバインドされるフィールドに対応するバインディングタグを設定する必要があることに注意してください。たとえば、JSONからバインドする場合は、json:"fieldname"を設定します。

また、特定のフィールドを必須フィールドとして指定することもできます。フィールドにbinding:"required"の修飾子が付いており、バインディング時に値が指定されていない場合は、エラーが返されます。

package main

import (
	"fmt"

	"github.com/kataras/iris/v12"
	"github.com/go-playground/validator/v10"
)

func main() {
	app := iris.New()
	app.Validator = validator.New()

	userRouter := app.Party("/user")
	{
		userRouter.Get("/validation-errors", resolveErrorsDocumentation)
		userRouter.Post("/", postUser)
	}
	app.Listen(":8080")
}

// Userはユーザー情報を含んでいます。
type User struct {
	FirstName      string     `json:"fname" validate:"required"` // 名、必須
	LastName       string     `json:"lname" validate:"required"` // 姓、必須
	Age            uint8      `json:"age" validate:"gte=0,lte=130"` // 年齢、0から130の範囲内
	Email          string     `json:"email" validate:"required,email"` // メール、必須
	FavouriteColor string     `json:"favColor" validate:"hexcolor|rgb|rgba"` // 好きな色、正しい16進カラーコード、RGB、またはRGBA値である必要があります
	Addresses      []*Address `json:"addresses" validate:"required,dive,required"` // 住所リスト、空でないこと、および各住所アイテムが必要です
}

// Addressはユーザーの住所情報を格納します。
type Address struct {
	Street string `json:"street" validate:"required"` // 住所、必須
	City   string `json:"city" validate:"required"` // 街、必須
	Planet string `json:"planet" validate:"required"` // 惑星、必須
	Phone  string `json:"phone" validate:"required"` // 電話、必須
}

type validationError struct {
	ActualTag string `json:"tag"` // 実際のタグ
	Namespace string `json:"namespace"` // 名前空間
	Kind      string `json:"kind"` // 種類
	Type      string `json:"type"` // タイプ
	Value     string `json:"value"` // 値
	Param     string `json:"param"` // パラメータ
}

func wrapValidationErrors(errs validator.ValidationErrors) []validationError {
	validationErrors := make([]validationError, 0, len(errs))
	for _, validationErr := range errs {
		validationErrors = append(validationErrors, validationError{
			ActualTag: validationErr.ActualTag(),
			Namespace: validationErr.Namespace(),
			Kind:      validationErr.Kind().String(),
			Type:      validationErr.Type().String(),
			Value:     fmt.Sprintf("%v", validationErr.Value()),
			Param:     validationErr.Param(),
		})
	}

	return validationErrors
}

func postUser(ctx iris.Context) {
	var user User
	err := ctx.ReadJSON(&user)
	if err != nil {
		// エラーを処理する、次の方法が正しいです...

		if errs, ok := err.(validator.ValidationErrors); ok {
			// JSON形式でエラーをラップする、基礎となるライブラリはインターフェース型のエラーを返します
			validationErrors := wrapValidationErrors(errs)

			// アプリケーション/JSON+problemのレスポンスを返し、後続のハンドラの実行を停止します
			ctx.StopWithProblem(iris.StatusBadRequest, iris.NewProblem().
				Title("検証エラー").
				Detail("1つ以上のフィールドが検証を通過しませんでした").
				Type("/user/validation-errors").
				Key("errors", validationErrors))

			return
		}

		// 内部JSONエラーかもしれません、ここではさらなる情報は提供されません。
		ctx.StopWithStatus(iris.StatusInternalServerError)
		return
	}

	ctx.JSON(iris.Map{"message": "OK"})
}

func resolveErrorsDocumentation(ctx iris.Context) {
	ctx.WriteString("このページは、Web開発者やAPIユーザーが検証エラーを解決する方法を説明するために使用されます")
}
{
    "title": "検証エラー",
    "detail": "1つ以上のフィールドが検証に失敗しました",
    "type": "http://localhost:8080/user/validation-errors",
    "status": 400,
    "fields": [
        {
            "tag": "required",
            "namespace": "User.FirstName",
            "kind": "string",
            "type": "string",
            "value": "",
            "param": ""
        },
        {
            "tag": "required",
            "namespace": "User.LastName",
            "kind": "string",
            "type": "string",
            "value": "",
            "param": ""
        }
    ]
}

バインディングURLクエリパラメータ

ReadQueryメソッドは、クエリパラメータのみをバインドし、リクエストボディデータはバインドしません。リクエストボディデータをバインドするには、ReadFormを使用します。

package main

import "github.com/kataras/iris/v12"

type Person struct {
    Name    string `url:"name,required"`
    Address string `url:"address"`
}

func main() {
    app := iris.Default()
    app.Any("/", index)
    app.Listen(":8080")
}

func index(ctx iris.Context) {
    var person Person
    if err := ctx.ReadQuery(&person); err != nil {
        ctx.StopWithError(iris.StatusBadRequest, err)
        return
    }

    ctx.Application().Logger().Infof("Person: %#+v", person)
    ctx.WriteString("Success")
}

任意のデータのバインディング

クライアントが送信したデータのコンテンツタイプに基づいて、リクエストボディを "ptr" にバインドします。対応するデータ形式には、JSON、XML、YAML、MessagePack、Protobuf、Form、およびURLクエリが含まれます。

package main

import (
    "time"
    "github.com/kataras/iris/v12"
)

type Person struct {
    Name       string    `form:"name" json:"name" url:"name" msgpack:"name"`
    Address    string    `form:"address" json:"address" url:"address" msgpack:"address"`
    Birthday   time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1" json:"birthday" url:"birthday" msgpack:"birthday"`
    CreateTime time.Time `form:"createTime" time_format:"unixNano" json:"create_time" url:"create_time" msgpack:"createTime"`
    UnixTime   time.Time `form:"unixTime" time_format:"unix" json:"unix_time" url:"unix_time" msgpack:"unixTime"`
}

func main() {
    app := iris.Default()
    app.Any("/", index)
    app.Listen(":8080")
}

func index(ctx iris.Context) {
    var person Person
    if err := ctx.ReadBody(&person); err != nil {
        ctx.StopWithError(iris.StatusBadRequest, err)
        return
    }

    ctx.Application().Logger().Infof("Person: %#+v", person)
    ctx.WriteString("Success")
}

以下のようにコマンドを使用してテストできます:

$ curl -X GET "localhost:8085/testing?name=kataras&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"

バインディングURLパスパラメータ

package main

import "github.com/kataras/iris/v12"

type myParams struct {
    Name string   `param:"name"`
    Age  int      `param:"age"`
    Tail []string `param:"tail"`
}

func main() {
    app := iris.Default()
    app.Get("/{name}/{age:int}/{tail:path}", func(ctx iris.Context) {
        var p myParams
        if err := ctx.ReadParams(&p); err != nil {
            ctx.StopWithError(iris.StatusInternalServerError, err)
            return
        }

        ctx.Writef("myParams: %#v", p)
    })
    app.Listen(":8088")
}

リクエスト

$ curl -v http://localhost:8080/kataras/27/iris/web/framework

ヘッダー要求パラメータのバインディング

package main

import "github.com/kataras/iris/v12"

type myHeaders struct {
    RequestID      string `header:"X-Request-Id,required"`
    Authentication string `header:"Authentication,required"`
}

func main() {
    app := iris.Default()
    r.GET("/", func(ctx iris.Context) {
        var hs myHeaders
        if err := ctx.ReadHeaders(&hs); err != nil {
            ctx.StopWithError(iris.StatusInternalServerError, err)
            return
        }

        ctx.JSON(hs)
    })
    
    app.Listen(":8080")
}

リクエスト

curl -H "x-request-id:373713f0-6b4b-42ea-ab9f-e2e04bc38e73" -H "authentication: Bearer my-token" \
http://localhost:8080

レスポンス

{
  "RequestID": "373713f0-6b4b-42ea-ab9f-e2e04bc38e73",
  "Authentication": "Bearer my-token"
}