Skip to main content

Data Binding

Parsing request data is a critical part of web applications. When clients initiate HTTP requests, they provide information in the following parts, and in Slim, data parsing can be accomplished through a "binding process":

  • URL path parameters
  • URL query parameters
  • Request headers
  • Request body

Slim provides different binding execution methods, each of which will be introduced below.

Struct Tags

We can define a Go struct and specify data sources through corresponding tags. In request handler functions, using the Context#Bind method allows us to bind submitted parameters to the struct.

In the example below, we specify the query tag for the struct field ID to tell the binder to bind parameters from the query string to User#ID:

Struct binding example
type User struct {
ID string `query:"id"`
}

// in the handler for /users?id=<userID>
var user User
err := c.Bind(&user)
if err != nil {
return c.String(http.StatusBadRequest, "bad request")
}

Data Sources

The framework supports the following tags to specify data sources:

  • query - Associates with URL query parameters
  • param - Associates with URL path parameters
  • header - Associates with request header information
  • json - Associates with submitted JSON data
  • xml - Associates with submitted XML data
  • form - Associates with submitted form data (including request body and query parameters)

Data Types

We combine the value of the Content-Type header to automatically select the appropriate program to parse the request body, supporting the following three ways of submitting data:

  • application/json - Submit data in JSON format
  • application/xml - Submit data in XML format
  • application/x-www-form-urlencoded - Submit data in form format

When binding path parameters, query parameters, request headers, or form data, tags must be explicitly set on struct fields. If tags are omitted, then JSON or XML is used to bind to struct field names.

For form data, we use Go's standard library to parse forms. If the content type is not MIMEMultipartForm, it will attempt to parse form data from both the request URL and request body.

Reference links:

Multiple Source Binding

Multiple sources can be specified on the same field. In this case, request data is bound in the following order:

  1. Path parameters
  2. Query parameters (only for GET and DELETE requests)
  3. Request body data
Specifying multiple data sources
type User struct {
ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"`
}
warning

Because data binding is executed sequentially according to the above method, overriding may occur. For example, if our request query parameter contains name=query, and the request body contains {"name":"body"}, the result will be User{Name: "body"}.

Single Source Binding

Use a specified single data source to bind data:

Bind request body data
err := slim.BindBody(c, &payload)
Bind query parameters
err := slim.BindQueryParams(c, &payload)
Bind route parameters
err := slim.BindPathParams(c, &payload)
Bind HTTP request headers
err := (&slim.DefaultBinder{}).BindHeaders(c, &payload)

Security

To ensure application security, if the bound struct instance contains fields that should not be bound, we should avoid passing this struct instance directly to other methods, and should explicitly map it to a business structure.

Consider what would happen if we have an exportable boolean field IsAdmin in the struct we need to bind, and the request body data contains {IsAdmin: true, Name: "hacker"}.

In the example below, we define a User struct type that contains field tags json, form, and query for binding data:

Data structure
type User struct {
Name string `json:"name" form:"name" query:"name"`
Email string `json:"email" form:"email" query:"email"`
}

type UserDTO struct {
Name string
Email string
IsAdmin bool
}

Then define a route POST /users to handle the request and bind data to the struct:

Route handler function
s.POST("/users", func(c slim.Context) (err error) {
u := new(User)
err := c.Bind(u)
if err != nil {
return c.String(http.StatusBadRequest, "bad request")
}

// Load into separate struct for security
user := UserDTO{
Name: u.Name,
Email: u.Email,
IsAdmin: false // avoids exposing field that should not be bound
}

// Execute our business logic
executeSomeBusinessLogic(user)

return c.JSON(http.StatusOK, u)
})

JSON Data

Simulate browser sending JSON data
curl -X POST http://localhost:1323/users \
-H 'Content-Type: application/json' \
-d '{"name":"Joe","email":"joe@labstack"}'

Form Data

Simulate browser sending form data
curl -X POST http://localhost:1323/users \
-d 'name=Joe' \
-d 'email=joe@labstack.com'

Custom Binder

As mentioned in the previous Customization chapter, by setting the Slim instance property Slim#Binder, we can register a custom data binder.

Custom data binding
type CustomBinder struct {}

func (cb *CustomBinder) Bind(c slim.Context, i any) error {
// We can use the default binder
db := new(slim.DefaultBinder)
err := db.Bind(i, c)
if err != slim.ErrUnsupportedMediaType {
return err
}
// Implement our own parameter binding logic
return nil
}

s.Binder = new(CustomBinder)