Validate phone numbers with go-playground/validator
Phone number validation in Go validator that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Go validator's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the Go guide; the rest is Go validator's own validation hook, a few lines long.
1. The file
Take phone.go from the Go guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs Go 1.21 or later; standard library only. Its Parse reads a number as typed and returns the number with its E.164 form, region and line type, or false as its second value when no plan accepts it.
go get github.com/go-playground/validator/v102. The Go validator side
signup/signup.go
The tag, registered once, and a struct that uses it.
package signup
import (
"github.com/go-playground/validator/v10"
"example.com/app/phone"
)
// RegisterPhone adds the "phone" tag: the field must be a number readable in the plans of package phone.
// "phone=GB" reads numbers typed without their country code in GB's plan.
func RegisterPhone(v *validator.Validate) error {
return v.RegisterValidation("phone", func(fl validator.FieldLevel) bool {
_, ok := phone.Parse(fl.Field().String(), fl.Param())
return ok
})
}
// Signup is a form as it arrives.
type Signup struct {
Name string `validate:"required"`
Phone string `validate:"required,phone=GB"`
}3. The tests
signup/signup_test.go: The tests, with the testing package.
package signup
import (
"errors"
"testing"
"github.com/go-playground/validator/v10"
)
func TestPhoneTag(t *testing.T) {
v := validator.New(validator.WithRequiredStructEnabled())
if err := RegisterPhone(v); err != nil {
t.Fatal(err)
}
for _, c := range []struct {
phone string
ok bool
}{
{"07400 123456", true},
{"+1 201-555-0123", true},
{"+44 7400 12345", false},
{"", false},
} {
err := v.Struct(Signup{Name: "Ada", Phone: c.phone})
if (err == nil) != c.ok {
t.Errorf("%q: got %v, want ok=%v", c.phone, err, c.ok)
}
var errs validator.ValidationErrors
if !c.ok && (!errors.As(err, &errs) || errs[0].Field() != "Phone") {
t.Errorf("%q: the error should be on Phone: %v", c.phone, err)
}
}
}go test ./...Run before publishing with github.com/go-playground/validator/v10 v10.30.5 and the file generated for GB, US, FR, as 2026-10-25.
What is specific to Go validator
- Gin validates
binding:"…"tags with this package: register the tag on its engine,binding.Validator.Engine().(*validator.Validate), and writebinding:"required,phone=GB". fl.Param()is the text after=; with no parameter it is empty, andParsereads only numbers written with their country code.- Validation answers yes or no; call
phone.Parseagain in the handler for the E.164 form to store.