go
69 lines · 9 steps
Turning Gin validation errors into JSON
A Gin handler binds and validates a request, then maps validator failures into structured, per-field error messages.
Explained by
highlit
1package api
2
3import (
4 "errors"
5 "net/http"
6
7 "github.com/gin-gonic/gin"
8 "github.com/go-playground/validator/v10"
9)
10
11type CreateUserRequest struct {
12 Email string `json:"email" binding:"required,email"`
13 Username string `json:"username" binding:"required,min=3,max=32,alphanum"`
14 Password string `json:"password" binding:"required,min=8"`
15 Age int `json:"age" binding:"required,gte=13,lte=120"`
16}
17
18type FieldError struct {
19 Field string `json:"field"`
20 Message string `json:"message"`
21}
22
23func (h *Handler) CreateUser(c *gin.Context) {
24 var req CreateUserRequest
25 if err := c.ShouldBindJSON(&req); err != nil {
26 var verrs validator.ValidationErrors
27 if errors.As(err, &verrs) {
28 fields := make([]FieldError, 0, len(verrs))
29 for _, fe := range verrs {
30 fields = append(fields, FieldError{
31 Field: fe.Field(),
32 Message: messageFor(fe),
33 })
34 }
35 c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": fields})
36 return
37 }
38 c.JSON(http.StatusBadRequest, gin.H{"error": "malformed JSON body"})
39 return
40 }
41
42 user, err := h.users.Create(c.Request.Context(), req.Email, req.Username, req.Password, req.Age)
43 if err != nil {
44 c.JSON(http.StatusInternalServerError, gin.H{"error": "could not create user"})
45 return
46 }
47 c.JSON(http.StatusCreated, user)
48}
49
50func messageFor(fe validator.FieldError) string {
51 switch fe.Tag() {
52 case "required":
53 return "this field is required"
54 case "email":
55 return "must be a valid email address"
56 case "min":
57 return "must be at least " + fe.Param() + " characters"
58 case "max":
59 return "must be at most " + fe.Param() + " characters"
60 case "alphanum":
61 return "must contain only letters and numbers"
62 case "gte":
63 return "must be " + fe.Param() + " or greater"
64 case "lte":
65 return "must be " + fe.Param() + " or less"
66 default:
67 return "is invalid"
68 }
69}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Struct binding tags let you declare validation rules right next to the fields they govern.
- 2Distinguishing malformed JSON from validation failures lets you return the right HTTP status for each.
- 3Translating raw validator tags into human messages keeps your API responses clean and client-friendly.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
go
package streaming import ( "bufio"
Streaming NDJSON logs over HTTP in Go
http-streaming
channels
select
Advanced
10 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
go
package api import ( "crypto/sha256"
ETag conditional requests in Gin
http-caching
etag
conditional-requests
Intermediate
6 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/turning-gin-validation-errors-into-json-explained-go-6264/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.