Learn Echo - Binding & Validation
Series/Learn Echo/Episode 5
Episode 5 of 23

Learn Echo - Binding & Validation

This episode dissects binding and validation in Echo: how c.Bind turns JSON, XML, or form data into a struct, struct tags for query and URI binding, custom binders, go-playground validator integration, and centralized 400 error handling.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Every REST API receives input from clients — and input is the most error-prone point. Echo solves these two problems at once: binding to turn payloads into Go structs, and validation to ensure incoming data is valid before it touches your business logic.

Episode 5 dissects both: how c.Bind handles JSON, XML, and form data; struct tags for query and URI binding; custom binders for special formats; the pluggable go-playground/validator; and the pattern for centralized 400 error handling.

Binding Basics with c.Bind

From Payload to Struct

c.Bind reads the request body and fills it into the struct you provide. The payload type is detected automatically from the Content-Type header:

Binding JSON payload
type CreateUserRequest struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}
 
e.POST("/users", func(c echo.Context) error {
	req := new(CreateUserRequest)
	if err := c.Bind(req); err != nil {
		return err
	}
	return c.JSON(http.StatusCreated, req)
})

When the client sends JSON {"name":"Arman","email":"arman@dev.id"}, c.Bind fills the struct and returns an error if the payload doesn't match. Notice the c.Bind(req) pattern — the struct must be passed as a pointer.

Supported Content-Types

Echo's default binder handles several formats at once:

  • JSON: used when Content-Type: application/json.
  • XML: used when Content-Type: application/xml.
  • Form: used when Content-Type: application/x-www-form-urlencoded.
  • Multipart: used for multipart/form-data.
Testing binding with curl
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Arman","email":"arman@dev.id"}'

Struct Tags for Other Sources

Query and URI Binding

Binding isn't only from the body. Echo supports the query and param tags so path parameters and query strings bind directly into a struct as well:

Binding from query and param
type ListFilter struct {
	Page    int    `query:"page"`
	Search  string `query:"q"`
	UserID  string `param:"id"`
}
 
e.GET("/users/:id", func(c echo.Context) error {
	f := new(ListFilter)
	if err := c.Bind(f); err != nil {
		return err
	}
	return c.JSON(http.StatusOK, f)
})

With c.Bind(f), the Page field is filled from ?page=, Search from ?q=, and UserID from the :id path parameter. A single bind call handles all input sources.

Note that c.Bind only parses data — it doesn't validate meaning. A payload like {"name":"","email":"bukan-email"} binds successfully, and that's where the validator steps in.

go-playground Validator Integration

Wiring the Validator into Echo

Echo doesn't ship a built-in validator; it provides an interface any library can fill. The most common one is go-playground/validator. Wire it up through the e.Validator property:

Registering the validator
import (
	"github.com/go-playground/validator/v10"
)
 
type CustomValidator struct {
	validator *validator.Validate
}
 
func (cv *CustomValidator) Validate(i interface{}) error {
	return cv.validator.Struct(i)
}
 
e.Validator = &CustomValidator{validator: validator.New()}

The CustomValidator struct implements the echo.Validator interface with the Validate method. After this, validation tags on your structs are processed automatically.

Validation Tags on Structs

go-playground/validator provides hundreds of rules through struct tags. Add tags like required, email, and gte:

Struct with validation tags
type CreateUserRequest struct {
	Name  string `json:"name" validate:"required,min=3"`
	Email string `json:"email" validate:"required,email"`
	Age   int    `json:"age" validate:"gte=0,lte=130"`
}

The rule required,min=3 means the field is mandatory and at least three characters long, email validates the email format, and gte/lte bounds the numeric range. The validator runs automatically when the handler calls c.Validate.

Custom Validation and Validation in Handlers

Validating After Binding

The correct pattern is to bind first, then validate:

Bind then validate
e.POST("/users", func(c echo.Context) error {
	req := new(CreateUserRequest)
	if err := c.Bind(req); err != nil {
		return err
	}
	if err := c.Validate(req); err != nil {
		return err
	}
	return c.JSON(http.StatusCreated, req)
})

c.Validate(req) invokes the registered validator and returns a validation error if any rule fails.

Writing Custom Validation Rules

For special needs, register your own validation function:

Custom validator
cv.validator.RegisterValidation("strong", func(fl validator.FieldLevel) bool {
	value := fl.Field().String()
	return len(value) >= 8 && value != strings.ToLower(value)
})

This function adds a new rule named strong that you can use in any struct tag: validate:"required,strong".

Centralized 400 Error Handling

Recognizing Binding and Validation Errors

Echo splits errors into two groups: HTTPError, which already has a status code, and plain errors. Errors from the binder and validator are plain errors, so they default to 500. To keep your API consistent, recognize both types and turn them into 400:

Turning errors into 400
if err := c.Bind(req); err != nil {
	return echo.NewHTTPError(http.StatusBadRequest, "format payload tidak valid")
}
if err := c.Validate(req); err != nil {
	return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

With this pattern, every input error produces a consistent 400 response.

Closing

Episode 5 makes you master Echo's input pipeline: c.Bind turns JSON, XML, or form data into a struct; the query and param tags capture input from the URL; go-playground/validator validates the meaning of the data; custom rules handle special needs; and 400 errors are handled centrally.

Key takeaways:

  • c.Bind parses the body according to Content-Type into a struct pointer.
  • The query and param tags bind URL values into a struct.
  • c.Bind only parses; c.Validate checks the meaning.
  • The validator is pluggable; go-playground is the most common choice.
  • validate tags like required and email run automatically.
  • Binding and validation errors should be turned into 400 HTTPErrors.

In episode 6 next, we'll discuss middleware — how to write middleware with c.Next(), registering it at the root, group, and route levels, pre-middleware, and built-in middleware like CORS, JWT, BodyLimit, Gzip, and RateLimit.

Learn Echo - Binding & Validation | Learn Echo