Skip to content

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/v10

2. 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 write binding:"required,phone=GB".
  • fl.Param() is the text after =; with no parameter it is empty, and Parse reads only numbers written with their country code.
  • Validation answers yes or no; call phone.Parse again in the handler for the E.164 form to store.