go 57 lines · 8 steps

Custom validators and binding in Gin

Register a reusable phone-number rule with Gin's validator and turn binding failures into structured field-level error responses.

Explained by highlit
1package handlers
2 
3import (
4 "net/http"
5 "regexp"
6 "sync"
7 
8 "github.com/gin-gonic/gin"
9 "github.com/gin-gonic/gin/binding"
10 "github.com/go-playground/validator/v10"
11)
12 
13var (
14 phoneRegex = regexp.MustCompile(`^\+?[1-9]\d{1,14}$`)
15 registerPhone sync.Once
16)
17 
18func RegisterValidators() {
19 registerPhone.Do(func() {
20 if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
21 v.RegisterValidation("e164", func(fl validator.FieldLevel) bool {
22 return phoneRegex.MatchString(fl.Field().String())
23 })
24 }
25 })
26}
27 
28type CreateContactRequest struct {
29 Name string `json:"name" binding:"required,min=2"`
30 Phone string `json:"phone" binding:"required,e164"`
31 Email string `json:"email" binding:"required,email"`
32}
33 
34func CreateContact(c *gin.Context) {
35 var req CreateContactRequest
36 if err := c.ShouldBindJSON(&req); err != nil {
37 var verrs validator.ValidationErrors
38 if errors.As(err, &verrs) {
39 fields := make(map[string]string, len(verrs))
40 for _, fe := range verrs {
41 fields[fe.Field()] = fe.Tag()
42 }
43 c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": fields})
44 return
45 }
46 c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
47 return
48 }
49 
50 contact, err := contacts.Create(c.Request.Context(), req.Name, req.Phone, req.Email)
51 if err != nil {
52 c.JSON(http.StatusInternalServerError, gin.H{"error": "could not create contact"})
53 return
54 }
55 
56 c.JSON(http.StatusCreated, contact)
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Registering a custom validation tag once lets every request struct reuse it declaratively via binding tags.
  2. 2Type-asserting binding errors to validator.ValidationErrors lets you report exactly which fields failed and why.
  3. 3Distinguishing validation failures from malformed input maps cleanly onto different HTTP status codes.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Custom validators and binding in Gin — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code