The previous sections introduced the method of directly reading request parameters. If it's cumbersome to read each parameter individually, the iris framework also provides a parameter binding mechanism, which can bind request parameters to a struct, and also supports a form parameter validation mechanism.

Model Binding and Validation

To bind the request body to a type, use model binding. Currently, we support binding types such as JSON, JSONProtobuf, Protobuf, MsgPack, XML, YAML, and standard form values (foo=bar&boo=baz).

// Below are the function definitions for binding request parameters of various formats to a 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

When using ReadBody, Iris will infer the binder based on the Content-Type header. If you are certain about the content you want to bind, you can use specific ReadXXX methods, such as ReadJSON or ReadProtobuf.

ReadBody(ptr interface{}) error

Iris comes with intelligent built-in data validation. However, it allows you to attach a validator that will be automatically called on methods like ReadJSON, ReadXML, etc. In this example, we will learn how to use go-playground/validator/v10 to validate the request body.

Please note that you need to set the corresponding binding tags on all fields to be bound. For example, when binding from JSON, set json:"fieldname".

You can also specify certain fields as required fields. If a field has the binding:"required" decoration and no value is provided during binding, an error will be returned.

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 contains user information.
type User struct {
    FirstName      string     `json:"fname" validate:"required"` // First name, required
    LastName       string     `json:"lname" validate:"required"` // Last name, required
    Age            uint8      `json:"age" validate:"gte=0,lte=130"` // Age, range between 0 and 130
    Email          string     `json:"email" validate:"required,email"` // Email, required
    FavouriteColor string     `json:"favColor" validate:"hexcolor|rgb|rgba"` // Favorite color, must be a legal hexadecimal, RGB, or RGBA color value
    Addresses      []*Address `json:"addresses" validate:"required,dive,required"` // Address list, must not be empty and each address item is required
}

// Address stores user address information.
type Address struct {
    Street string `json:"street" validate:"required"` // Street, required
    City   string `json:"city" validate:"required"` // City, required
    Planet string `json:"planet" validate:"required"` // Planet, required
    Phone  string `json:"phone" validate:"required"` // Phone, required
}

type validationError struct {
    ActualTag string `json:"tag"` // Actual tag
    Namespace string `json:"namespace"` // Namespace
    Kind      string `json:"kind"` // Kind
    Type      string `json:"type"` // Type
    Value     string `json:"value"` // Value
    Param     string `json:"param"` // Parameter
}

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 {
        // Handle errors, the following is the correct way...

        if errs, ok := err.(validator.ValidationErrors); ok {
            // Wrap errors in JSON format, the underlying library returns errors of type interface.
            validationErrors := wrapValidationErrors(errs)

            // Return an application/json+problem response and stop executing subsequent handlers
            ctx.StopWithProblem(iris.StatusBadRequest, iris.NewProblem().
                Title("Validation Errors").
                Detail("One or more fields did not pass validation").
                Type("/user/validation-errors").
                Key("errors", validationErrors))

            return
        }

        // It might be an internal JSON error, no further information provided here.
        ctx.StopWithStatus(iris.StatusInternalServerError)
        return
    }

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

func resolveErrorsDocumentation(ctx iris.Context) {
    ctx.WriteString("This page is used to explain how to resolve validation errors to web developers or API users")
}

Sample Request

{
    "fname": "",
    "lname": "",
    "age": 45,
    "email": "[email protected]",
    "favColor": "#000",
    "addresses": [{
        "street": "Eavesdown Docks",
        "planet": "Persphone",
        "phone": "none",
        "city": "Unknown"
    }]
}

Sample Response

{
    "title": "Validation Error",
    "detail": "One or more fields failed validation",
    "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": ""
        }
    ]
}

Binding URL query parameters

The ReadQuery method binds only the query parameters, not the request body data. Use ReadForm to bind the request body data.

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

Binding arbitrary data

Bind the request body to "ptr" based on the content type of the data sent by the client, such as JSON, XML, YAML, MessagePack, Protobuf, Form, and URL query.

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

You can test with the following command:

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

Binding URL path parameters

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

Request

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

Binding header request parameters

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

Request

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

Response

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