Back to

package zvalidate

Latest Go to latest
Published: Mar 16, 2020 | License: MIT | Module:


Build Status codecov GoDoc

Simple validation for Go. Some things that make it different from the (many) other libraries:

  • No struct tags – which I don’t find a good tool for this – just functions.
  • Validations return parsed values.
  • Easy to display validation errors in UI.
  • Doesn’t use reflection (other than type assertions); mostly typed.
  • No external dependencies.
  • Easy to add nested validations.
  • Not tied to HTTP (useful for validating CLI flags, for example).

I originally wrote this at my previous employer (, this is an improved (and incompatible) version.

Basic example:

email := ""

v := zvalidate.New()
v.Required("email", email)
m := v.Email("email", email)

if v.HasErrors() {
    fmt.Printf("Had the following validation errors:\n%s", v)

fmt.Printf("parsed email: %s\n", m.Address)

All validators are just method calls on the Validator struct, and follow the same patterns:

  • The input’s zero type (empty string, 0, nil, etc.) is valid. Use the Required() validator if you want to make a parameter required).

  • key string, value [..] are the first two arguments, where key is the parameter name (to display in the error or next to a form) and value is what we want validated (type of value depends on validation).

  • Optionally accept a custom message as the last parameter.

The error text only includes a simple human description such as “must be set” or “must be a valid email”. When adding new validations, make sure that they can be displayed properly when joined with commas. A text such as “Error: this field must be higher than 42” would look weird:

must be set, Error: this field must be higher than 42


List of validations with abbreviated function signature (key string, value [..] omitted):

Function Description
Required() Value must not be the type’s zero value
Exclude([]string) Value is not in the exclude list
Include([]string) Value is in the include list
Range(min, max int) Minimum and maximum int value.
Len(min, max int) int Character length of string
Integer() int64 Integer value
Boolean() bool Boolean value
Domain() []string Domain name; returns list of domain labels.
URL() *url.URL Valid URL
Email() mail.Address Email address
IPv4() net.IP IPv4 address
IP() net.IP IPv4 or IPv6 address
HexColor() (uint8, uint8, uint8) Colour as hex triplet (#123456 or #123)
Date(layout string) Parse according to the given layout
Phone() string Looks like a phone number

You can set your own errors with v.Append():

if !some_complex_condition {
    v.Append("foo", "must be a valid foo")

Nested validations

Sub() allows adding nested subvalidations; this is useful if a form creates more than one object.

For example:

v := zvalidate.New()
v.Sub("settings", -1, customer.Settings.Validate())

This will merge the Validator object in to v and prefix all the keys with settings., so you’ll have settings.timezone (instead of just timezone).

You can also add arrays:

for i, a := range customer.Addresses {
    a.Sub("addresses", i, c.Validate())

This will be added as addresses[0].city, addresses[1].city, etc.

If the error is not a Validator then the Error() text will be added as just the key name without subkey (same as v.Append("key", "msg")); this is mostly to support cases like:

func (Customer c) Validate() {
    v := validate.New()

    ok, err := c.isUniqueEmail(c.Email)
    if err != nil {
        return err
    if !ok {
        v.Append("email", "must be unique")

    return v.ErrorOrNil()

Displaying errors

The Validator type satisfies the error interface, so you can return them as errors; usually you want to return ErrorOrNil().

The general idea is that validation errors should usually be displayed along the input element, instead of a list in a flash message (but you can do either).

  • To display a flash message or CLI just call String() or HTML().

  • For Go templates there is a TemplateError() helper which can be added to the template.FuncMap. See the godoc for that function for details and an example.

  • For JavaScript Errors is represented as map[string][]string, and marshals well to JSON; in your frontend you just have to find the input belonging to the map key. A simple example might be:

  var display_errors = function(errors) {
      var hidden = '';
      for (var k in errors) {
          if (!errors.hasOwnProperty(k))

          var elem = document.querySelector('*[name=' + k + ']')
          if (!elem) {
              hidden += k + ': ' + errors[k].join(', ');

          var err = document.createElement('span');
          err.className = 'err';
          err.innerHTML = 'Error: ' + errors[k].join(', ') + '.';
          elem.insertAdjacentElement('afterend', err);

      if (hidden !== '')

      'xxx':    ['oh noes', 'asd'],
      'hidden': ['asd'],

caveat: if there is an error with a corresponding form element then that won’t be displayed. This is why the above examples Pop() all the errors they want to display, and then display anything that’s left. This prevents “hidden” errors.


There is no direct support for i18n, but the messages are all exported as Message* and can be replaced by your i18n system of choice.