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:
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:
- non-MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseForm
- MIMEMultipartForm: https://golang.org/pkg/net/http/#Request.ParseMultipartForm
Multiple Source Binding
Multiple sources can be specified on the same field. In this case, request data is bound in the following order:
- Path parameters
- Query parameters (only for GET and DELETE requests)
- Request body data
type User struct {
ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"`
}
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:
err := slim.BindBody(c, &payload)
err := slim.BindQueryParams(c, &payload)
err := slim.BindPathParams(c, &payload)
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:
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:
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
curl -X POST http://localhost:1323/users \
-H 'Content-Type: application/json' \
-d '{"name":"Joe","email":"joe@labstack"}'
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.
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)