go 42 lines · 5 steps

Custom validators in Gin

Register reusable struct-tag validators on Gin's binding engine, safely and exactly once.

Explained by highlit
1package validators
2 
3import (
4 "regexp"
5 "sync"
6 "time"
7 
8 "github.com/gin-gonic/gin/binding"
9 "github.com/go-playground/validator/v10"
10)
11 
12var (
13 slugPattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
14 once sync.Once
15)
16 
17func Register() {
18 once.Do(func() {
19 v, ok := binding.Validator.Engine().(*validator.Validate)
20 if !ok {
21 return
22 }
23 v.RegisterValidation("slug", validateSlug)
24 v.RegisterValidation("notpast", validateNotPast)
25 })
26}
27 
28func validateSlug(fl validator.FieldLevel) bool {
29 s := fl.Field().String()
30 if s == "" {
31 return true
32 }
33 return slugPattern.MatchString(s)
34}
35 
36func validateNotPast(fl validator.FieldLevel) bool {
37 ts, ok := fl.Field().Interface().(time.Time)
38 if !ok {
39 return false
40 }
41 return !ts.Before(time.Now())
42}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Registering validators through sync.Once keeps setup idempotent even if called from multiple init paths.
  2. 2Custom validators are plain functions that read fl.Field() and return a bool verdict.
  3. 3Treating an empty string as valid lets you compose a format rule with a separate required rule.

Related explainers

Share this explainer

Here's the card — post it anywhere.

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