# go-playground/validator: struct tags as a validation contract

> The validator package turns struct tags into a validation contract, then hands back a typed error you can inspect field by field. Its repository layout explains what that contract can and cannot do.

**go-playground/validator** — :100:Go Struct and Field validation, including Cross Field, Cross Struct, Map, Slice and Array diving

- Repository: https://github.com/go-playground/validator
- Stars: 20,193 · Forks: 1,475
- Language: Go
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/go-playground-validator

## Struct tags as the place where rules live

The package implements value validations for structs and individual fields based on tags. That single sentence describes the whole architecture. A validation rule lives next to the field it constrains, as a string in the struct tag, and the library reflects over the struct at runtime to find them. No registration call, no schema file, no code generation step between you and the rule.

The breadth of what a tag can express is the reason the package is widely used. The README's baked-in catalog is organized into categories, and the categories are more informative than the individual tags. The fields group is cross-field comparison: `eqfield`, `eqcsfield`, `nefield`, `gtfield`, `gtefield`, `ltfield`, `ltefield` and their relative variants, which compare one field against another rather than against a constant. The network group covers address and protocol shapes including `cidr`, `cidrv4`, `cidrv6`, `datauri`, `fqdn`, `hostname`, `hostname_rfc1123`, `hostname_port`, `port`, the `ip` family, the `tcp`, `udp` and `unix` address families, the `url` family, `origin` for a scheme and host with no path, and URN validation in two RFC versions. The strings group starts with the alphanumeric family, `alpha`, `alphaspace`, `alphanum`, `alphanumspace`, `alphanumunicode` and the numeric variants.

A rule that no built-in tag fits is not a reason to give up on the package. Custom validators can be registered, and struct level validation exists for rules that compare a whole struct rather than one field. What you lose with custom functions is the part that matters most in practice: the error comes back tagged with the field and the rule name, so it can be localized and mapped to a form field without extra bookkeeping.

## Why validation returns an error you have to type assert

Validation functions return the type `error`, and the README gives the reason in a sentence that reads like a bug report from someone else's project. Returning `error` avoids the classic Go problem of a method that always returns a non-nil value, which is what happens when a function reports failure through a bool and the caller forgets to check. Issue 134 in the tracker and a Stack Overflow thread are both cited as the background.

The return contract is specific enough to be worth memorizing. The validator returns nil when the value passes, a `ValidationErrors` value when validation fails on the fields, and only an `InvalidValidationError` when the input to the validator itself was wrong, such as validating a value that is not a struct or a pointer to one. In practice that means a single nil check covers the common case, and a type assertion is only needed when you want to render the individual failures.

The assertion uses `errors.As` rather than a comma-ok type switch, because the value travels wrapped:

```go
err := validate.Struct(mystruct)
var validationErrors validator.ValidationErrors
errors.As(err, &validationErrors)
```

Three lines, and each one earns its place. `ValidationErrors` is a slice of `FieldError` values, so once the assertion succeeds you can range over failures and pull out the field name, the struct name, the tag that failed, the parameter it was given and the actual value. The README also calls out that you can register a function to extract a custom field name, such as the JSON name, so the field in the error matches what the client sent. That detail is the difference between an error message a user can act on and one they cannot.

## Cross field, cross struct, and one opt-in that changes behavior

The feature list opens with cross field and cross struct validation by using validation tags or custom validators, and that is the capability that separates this from a field-at-a-time checker. Some rules simply cannot be expressed per field. A start date that must precede an end date, a password and its confirmation, a quantity that has to be positive only when a discount is active: each of those needs to see two or more fields at once, which is what the relative field tags and struct level validation exist for.

The struct level path is a clean fit for Go's type system. Registering a validation for a struct type means the callback receives the whole value, so the rule can be written as ordinary Go logic with an ordinary error return. When the rule fails, the field is reported as the struct itself, which keeps the error shape uniform with field level failures. The repository backs this with a dedicated `struct_level.go` at the root and a runnable example under `_examples/`.

One behavior change deserves attention from anyone starting a new project. The README notes that people new to the package should initialize the validator with the `WithRequiredStructEnabled` option, which is opt-in now and becomes the default in v11 and later:

```go
validate := validator.New(validator.WithRequiredStructEnabled())
```

Turning it on at the start costs nothing and means the required-struct rule behaves the same way before and after your next major upgrade. Turning it on later means auditing every place that validated a struct, because the results change.

## Diving into slices, arrays and maps

The second feature on the list is diving, and it is the one that saves the most code in a typical service. Slice, array and map diving lets any level of a multidimensional field be validated, so a slice of structs is checked element by element without a hand written loop, and a slice of slices works the same way with another tag.

Maps get more attention than they usually get, because the README lists two separate capabilities: diving into values, and diving into keys. Validating map keys is unusual and occasionally necessary, since a key is often an identifier such as a tenant slug or an external reference that has a shape worth enforcing.

Two supporting behaviors make the diving reliable. The package resolves a value held in an interface by determining its underlying type first, so a `[]any` full of strings validates as strings rather than being skipped. It also handles custom field types, including the database driver's Valuer interface, so a type that renders itself for storage is unwrapped before validation instead of being rejected as an unknown kind.

The cost of all this is reflection at runtime, and the repository is candid about it: `cache.go` caches the compiled validation state for a type so the reflect work happens once per type rather than on every call, and `benchmarks_test.go` sits at the top level to keep that cost visible. Two root files handle registration: `validate_fn.go` for registering custom rules, and `no_validate_fn.go` for the opposite case, a way to mark a field as not requiring validation.

## Translations, codes and the files behind them

Customizable internationalization aware error messages are a listed feature, and the source files show how much of the package that actually takes. `translations.go` holds the message catalog, a `translations/` directory holds per language packages, `language_codes.go` enumerates the languages, `country_codes.go` and `currency_codes.go` back the regional validation tags, and `postcode_regexes.go` carries the per country postal code patterns.

The dependency list confirms the division of labor. `go-playground/locales` supplies the locale data and `go-playground/universal-translator` supplies the translation layer, while `leodido/go-urn` exists for URN parsing rather than validation alone, `gabriel-vasile/mimetype` for content type sniffing, and `golang.org/x/text` and `golang.org/x/crypto` for text processing and the hash based validators. The module declares `go 1.26.0` and publishes as `/v10`, which is why the import path carries the major version suffix.

The repository also carries the signs of a project looking for maintainers. There is a `A Call for Maintainers` section in the README pointing at a discussion thread, a `MAINTAINERS.md` at the root, a `CLAUDE.md` for agent-assisted contributions, a `.golangci.yaml` for the lint configuration, and a `non-standard/` directory for the tags that are not part of the standard catalog. A `_examples/` directory carries runnable programs for the simple case, custom field types, struct level validation, translations and gin integration.

The Makefile shows the verification loop, and it is a small one:

```makefile
test:
	$(GOCMD) test -cover -race ./...
```

Coverage and the race detector on every package, with a separate benchmark target that runs the benchmarks without executing tests, since `benchmarks_test.go` is part of the same module.

## Conclusion

The design of this package comes down to three decisions: tags carry the rules, the return value is an `error` that can be a list, and the vocabulary of built-in validators is wide enough that most structs need no hand written functions at all. Add struct level validation for rules that no tag can express, add a translation layer when your errors cross a language boundary, and remember that `WithRequiredStructEnabled` is a preview of the v11 defaults rather than a formatting preference. Installing is two lines, but the interesting reading is in the source files: `baked_in.go` for the catalog, `errors.go` for the error shape, `translations.go` for message catalogs, and `options.go` for the behavior changes coming in the next major version.

## FAQ

### How do I install and start using go-playground/validator?

The README gives two steps: fetch the module with `go get github.com/go-playground/validator/v10`, then import the package at the same versioned path. Detailed usage documentation lives on pkg.go.dev, and runnable examples for the simple case, custom field types, struct level validation and translations live under `_examples/` in the repository.

### What does a ValidationErrors value contain in go-playground/validator?

`ValidationErrors` is a slice of `FieldError` values, each carrying the field name, the struct name, the failing tag, the parameter and the actual value. Since the validator returns the type `error`, you get the list back with `errors.As` into a `validator.ValidationErrors` variable. It also returns an `InvalidValidationError` when the input to the validator itself was wrong, so a nil check covers the common case.

### What is the WithRequiredStructEnabled option?

The README recommends it for anyone new to the package. It is opt-in behavior around required struct validation today and becomes the default in v11 and later, so initializing with `validator.New(validator.WithRequiredStructEnabled())` at the start of a project avoids a behavioral change at your next major upgrade.

### How do I validate nested slices and maps of structs?

Use the diving tags, which validate any level of a multidimensional field. The package also validates values held in an interface by resolving the underlying type first, and it can dive into map keys as well as values. Both capabilities are listed in the README's feature list and exercised in the examples directory.

### Can I validate two fields against each other in go-playground/validator?

Yes, through the cross-field tags such as `eqfield`, `gtfield`, `ltefield` and their relative `csfield` variants, and through struct level validation when a rule needs to see the whole value. Custom validators can also be registered with `validate_fn.go`, which is the extension point when a built-in tag does not fit.

### How does go-playground/validator handle translated error messages?

Translations are a listed feature. The message catalog lives in `translations.go`, per language packages live under `translations/`, and the language enumeration is in `language_codes.go`, with regional support alongside `country_codes.go` and `currency_codes.go`. The dependencies on `go-playground/locales` and `go-playground/universal-translator` provide the locale data and the translation layer.

## Sources

- [go-playground/validator on GitHub](https://github.com/go-playground/validator)
- [Issues](https://github.com/go-playground/validator/issues)
- [License: MIT](https://github.com/go-playground/validator/blob/master/LICENSE)
- [README](https://github.com/go-playground/validator/blob/master/README.md)
- [Releases](https://github.com/go-playground/validator/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/go-playground-validator
