Help us shape the future of Go! Take the 2026 Go Developer Survey

validate

package module
v0.0.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 12 Imported by: 0

README

validate

CI codecov Go Reference Release 日本語

kanna-validate generates a Validate method from the validate tags of a struct — plain if statements, no reflection — and reports whatever breaks under Rails' error codes, keyed by the fields' JSON names.

Part of the kanna family of code generators. Requires Go 1.27, for encoding/json/v2.

Install

go get -tool github.com/go-kanna/validate/cmd/kanna-validate

The generated code calls the small runtime at the root of this module, github.com/go-kanna/validate, which the line above already requires.

Use

Mark a struct //kanna:validate and write its rules as you would for go-playground/validator:

package user

//go:generate go tool kanna-validate

//kanna:validate
type CreateUserRequest struct {
	Name     string   `json:"name"     validate:"required,max=50"`
	Age      int      `json:"age"      validate:"gte=18,lte=130"`
	Role     string   `json:"role"     validate:"required,oneof=admin member guest"`
	Tags     []string `json:"tags"     validate:"max=5,dive,max=20"`
	Address  Address  `json:"address"  validate:"required"`
	Password string   `json:"password" validate:"required,min=12"`
	Confirm  string   `json:"confirm"  validate:"eqfield=Password"`
}

//kanna:validate
type Address struct {
	Zip string `json:"zip" validate:"required,pattern=^\\d{3}-\\d{4}$"`
}

go generate ./... writes validate_gen.go next to it:

// Code generated by kanna-validate. DO NOT EDIT.

func (x CreateUserRequest) Validate() error {
	var errs validate.Errors

	if x.Name == "" {
		errs.Add("name", validate.Blank())
	} else {
		if utf8.RuneCountInString(x.Name) > 50 {
			errs.Add("name", validate.TooLong(50))
		}
	}

	if x.Age < 18 {
		errs.Add("age", validate.GreaterThanOrEqualTo(18))
	}
	// ...

	for i := range x.Tags {
		if utf8.RuneCountInString(x.Tags[i]) > 20 {
			errs.Add(validate.Index("tags", i), validate.TooLong(20))
		}
	}

	if x.Address == (Address{}) {
		errs.Add("address", validate.Blank())
	} else {
		errs.Nest("address", x.Address.Validate())
	}
	// ...

	return errs.Err()
}

Every rule is an if you can read and stop at in a debugger, and changing a tag changes the generated code, so a change of rules shows up in review.

A rule that does not exist or does not fit its field stops generation, rather than the first request that meets it:

$ go generate ./...
./request.go:5:2: error: unknown rule "requried" (field Name)
  did you mean "required"?
./request.go:6:2: error: rule "email" cannot apply to int (field Age)
./request.go:7:2: error: rule "max": 300 does not fit uint8 (field Level)
./request.go:8:2: error: invalid pattern: error parsing regexp: missing closing ]: `[0-9` (field Zip)
./request.go:10:2: error: eqfield=Pasword: no such field (field Confirm)
  did you mean "Password"?

A misspelled directive, which would otherwise leave the struct without a Validate and say nothing, draws a warning:

./request.go:3:1: warning: "//kanna:validte" is not recognized as a directive
  did you mean "//kanna:validate"?

Rules

The tags are go-playground/validator's; the codes and params are those of Rails' ActiveModel::Validations.

Tag Applies to Code Params
required anything with a zero value blank
min, max, len strings (in characters), slices, maps too_short, too_long, wrong_length count
min, max numbers greater_than_or_equal_to, less_than_or_equal_to count
gt, gte, lt, lte, eq, ne numbers greater_than, greater_than_or_equal_to, less_than, ..., other_than count
oneof=a b c strings, integers inclusion
eqfield=Field a field of the same type confirmation attribute: the other's JSON name
pattern=regexp strings invalid
email, url, uuid strings email, url, uuid, from go-kanna/value
rule=Type, rule=pkg.Type what Type holds or validates the rule's own the rule's own
omitempty anything with a zero value lets the zero value past the rules after it
dive slices, arrays, maps applies the rules after it to each element, or each value of a map

Where it differs from go-playground:

  • Every rule a field breaks is reported, not just the first. A value that fails required is the exception: it is reported blank alone, since anything else it breaks only says again that it is missing.
  • required means "not the zero value": "", 0, false, nil, or IsZero() for a type that has the method. An empty slice that is not nil is there; bound its length with min=1. A 0 sent is the 0 never sent, so where a 0, or a value type whose rule accepts its zero, is a value of its own, take a pointer: required then reports only nil.
  • A nil pointer skips every rule but required: a *string tagged max=20 accepts nil, one tagged required,max=20 does not.
  • A floating-point NaN breaks every comparison it is held to: min=0 and max=100 both report it. The generated code negates the requirement, !(x >= 0), as x < 0 would let NaN through.
  • pattern= takes the rest of the tag, commas included, so it comes last. The tag value is a Go string literal, so a backslash is written \\; a tag that does not unquote stops generation instead of being skipped in silence.
  • omitempty comes first among the rules of its level.

rule=Type applies the rule of Type to the field's value, or to each element after dive. Type is a value type of kanna-value, known by the shape kanna-value gives it, whose constructor NewType(T) (Type, error) holds a value to its rule; an enum of kanna-value, whose constructor NewType(string) (Type, error) accepts only its constants; or a type with a method Validate(T) error, called on its zero value. A bare Type is one of the struct's package. pkg.Type names the package by its name, among the packages of your module and those the struct's package imports, directly or not; a/pkg.Type, with as much of the import path as it takes, tells two packages of one name apart. The struct's file need not import it, since the generated file does. The generated code records at the field what the constructor or Validate returns, with the code and params the rule gave it.

Nesting and hooks

A field whose type has Validate() error — another //kanna:validate struct, an enum of kanna-value, a type validating itself by hand, an interface — is validated with it, and its errors are nested under the field: address.zip, items[2].sku, or labels.primary for a map. Slices, arrays, and maps of such types are validated element by element without a dive. An embedded struct without a JSON name keeps its errors at the level of the struct embedding it, as encoding/json flattens its fields.

A rule no tag can express, such as one spanning two fields, goes in a validate method, which the generated Validate calls after every tag has been checked:

func (x CreateUserRequest) validate(errs *validate.Errors) {
	if x.Role == "admin" && x.Age < 21 {
		errs.Add("age", validate.Fail("too_young_for_admin"))
	}
}

An error concerning the struct as a whole goes under validate.Base, "base", as in Rails.

Errors

Validate returns validate.Errors, or nil. It encodes as Rails encodes errors.details, with the fields in the order they first broke:

{
  "name": [{"error": "blank"}],
  "tags[1]": [{"error": "too_long", "count": 20}],
  "confirm": [{"error": "confirmation", "attribute": "password"}]
}

A field is keyed by the name its json tag gives it, or by its Go name without one, whatever the transport. Behind a gRPC handler, a use case input without json tags reports TodoID; tag the field json:"id" to report the name the proto declares. A message that protoc-gen-go generates carries json tags with the proto's field names already.

The rejected value is never part of an error: errors end up in logs, and the value may be personal data.

Decoding requests

validate.DecodeJSON decodes a request body, validates it, and reports everything wrong with it in one go:

var req user.CreateUserRequest
if err := validate.DecodeJSON(r.Body, &req); err != nil {
	if errs, ok := errors.AsType[validate.Errors](err); ok {
		writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"errors": errs})
		return
	}
	writeError(w, http.StatusBadRequest, err) // JSON that cannot be read at all
	return
}

A field whose type rejects its value while decoding — a value type of kanna-value, say — does not stop the decoding: the rejection is reported at the field's path, alongside whatever the tags find in the rest. json.Unmarshal cannot do this; it stops at the first rejection, and does not say which field it came from. DecodeJSON decodes through encoding/json/v2, which matches names case-sensitively and rejects duplicate names.

With kanna-value

kanna-value generates value types that hold a value to its rule wherever it goes: when it is built, decoded, or scanned from a database. The email, url, and uuid tags call the same rules, from its package rule, and report the same codes, so a format checked by a tag and one held in a type read alike in an API response.

//kanna:value
type Email struct{ s string }

func (Email) validate(s string) error { return rule.Email(s) }

//kanna:validate
type SignUp struct {
	Email   Email  `json:"email"   validate:"required"`      // the type holds the format; the tag, that it is there
	Website string `json:"website" validate:"omitempty,url"` // the tag checks the format of a plain string
}

An enum of kanna-value, a string type whose generated UnmarshalText and Validate accept only its constants, needs no oneof listing them again, and reports the same code, inclusion:

//kanna:validate
type ListOrders struct {
	Status *model.OrderStatus `json:"status"` // nil when omitted; "shiped" is reported as inclusion
}

rule= applies the rule of a value type to a plain field, through its constructor. A request built from something other than JSON, such as a gRPC message, can take a *time.Time and check it as model.CompletedAt would:

//kanna:validate
type UpdateTask struct {
	CompletedAt *time.Time `json:"completed_at" validate:"rule=model.CompletedAt"` // nil skips it
}
	if x.CompletedAt != nil {
		if _, err := model.NewCompletedAt(*x.CompletedAt); err != nil {
			errs.Add("completed_at", err)
		}
	}

It checks a plain string against the constants of an enum the same way, as rule=model.OrderStatus.

go-kanna/value is a module of its own: go get github.com/go-kanna/value when you use these tags, or go get -tool github.com/go-kanna/value/cmd/kanna-value for its types. A generated file that needs it while your go.mod does not require it gets a warning rather than a build that fails later:

./validate_gen.go: warning: imports github.com/go-kanna/value/rule, which go.mod does not require; run go mod tidy

Linting

wrapcheck reports return errs.Err() in a hand-written Validate as an unwrapped error from another package. Err hands back what was collected, with nothing underneath to wrap, so add it to the signatures wrapcheck ignores:

linters:
  settings:
    wrapcheck:
      extra-ignore-sigs:
        - validate.Errors).Err(

Flags

kanna-validate [flags] [packages]
Flag Meaning
-tags <list> comma-separated build tags
-check verify the output is up to date instead of writing it, including that no output is left over
-v list the files written, removed, or left unchanged
-version print the version

The packages default to the one in the current directory, so //go:generate go tool kanna-validate needs no argument. A run that succeeds prints nothing but its warnings. One of them is about the run itself: when no struct in its packages is marked //kanna:validate, it succeeds, since a //go:generate line left behind does no harm, but says so, since the line was most likely meant to go as well:

$ go generate ./...
warning: no struct is marked //kanna:validate in example.com/app/user

Code may call Validate before the first run has written it. kanna-validate type-checks the packages it loads once more with what it is about to write in place, and writes nothing while their code does not compile with it: a mistake the missing method hid is reported then, such as passing what Validate returns where it does not fit.

A package whose structs have all lost //kanna:validate has its validate_gen.go removed, unless code in the packages the run loads still calls their Validate; a file of that name without the generated-code header is never touched. The go generate that removes it ends with open ...: no such file or directory, from go generate itself, which lists a package's files before it runs the generators; the file is gone as it should be (golang/go#36068).

Example

example decodes sign-up requests the way a handler would — a valid one, one with everything wrong at once, an empty one, one the hook rejects, and malformed JSON — and prints the response each gets. CI regenerates it and fails if the output would change.

Performance

bench validates a request like the one in Use with the generated Validate and with go-playground/validator, both reading the same tags; len=8 stands in for pattern=, which go-playground does not have. go-playground is set up to report what kanna-validate does — a required struct must not be its zero value, and a field goes by its JSON name — and a test fails unless the two find the same fields broken.

Request Validator ns/op B/op allocs/op
valid kanna-validate 18 0 0
valid go-playground/validator v10.30.5 539 432 18
seven fields broken kanna-validate 231 1032 16
seven fields broken go-playground/validator v10.30.5 989 2210 51

The median of 10 runs with Go 1.27.3 on an Apple M5 Pro. The generated Validate checks a valid request about 30 times as fast as go-playground, and a broken one about 4 times as fast with 53% fewer bytes and 69% fewer allocations. A valid request costs the ifs and allocates nothing; a broken one costs the errors it reports, each built with its code and params.

make bench                       # run it once
make bench BENCHFLAGS=-count=10  # often enough for benchstat

Development

make test       # unit tests
make lint       # golangci-lint
make example    # regenerate the example, then build and run it
make bench      # regenerate the benchmark, then run it

License

MIT

Documentation

Overview

Package validate is the runtime that code generated by kanna-validate calls, and what the code inspecting its result works with.

A generated Validate method checks each field with plain Go and collects what broke into Errors: every broken rule rather than the first, each under the path its JSON names spell. The codes and params follow Rails' ActiveModel::Validations (blank, too_long with a count, ...), and Errors encodes as Rails encodes errors.details.

The generator itself is the command in cmd/kanna-validate.

Index

Constants

View Source
const Base = "base"

Base is the field of an error that concerns the struct as a whole rather than one of its fields, as in Rails.

Variables

This section is empty.

Functions

func DecodeJSON

func DecodeJSON(r io.Reader, v any) error

DecodeJSON decodes the JSON value r holds into v, which must be a non-nil pointer, and reports everything wrong with it at once.

A value whose type rejects it with a Coded error, as the value types of kanna-value do, does not stop the decoding: the error is recorded at the value's path and decoding goes on. The rejection may come from any method encoding/json/v2 decodes a type with: UnmarshalJSONFrom, UnmarshalJSON, or UnmarshalText. Then, when v has a Validate method, what it finds is added, except at and under the paths decoding rejected. Those values were never set, and checking them would only report the absence of a value that was in fact sent.

The result is the Errors of both, or nil. Anything else fails with encoding/json/v2's own error: JSON that is malformed, holds more than one value, or does not fit the Go types, and a rejection that is not Coded or that falls on a map key. Such a request cannot be read, rather than breaking a rule.

Decoding follows encoding/json/v2, which, unlike encoding/json, matches names case-sensitively and rejects duplicate names.

func Index

func Index(field string, i int) string

Index returns the path to element i of the slice at field, such as "tags[2]".

func Key

func Key[K comparable](field string, key K) string

Key returns the path to the value under key in the map at field, such as "labels.primary": a map is a JSON object, so its keys are spelled as the names of an object are, as DecodeJSON reports them.

Types

type Coded

type Coded interface {
	error
	Code() string
	Params() map[string]any
}

Coded is an error that names the rule a value broke.

The code is stable enough to key a translated message or an API response, and the params carry what such a message needs. *rule.Error from github.com/go-kanna/value/rule is Coded too, which is how a rule held in a value type and one checked by a tag end up in the same Errors without either package importing the other.

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error is the Coded error the built-in rules report. A validate hook reports its own through Fail and FailWith.

Error is used through a pointer: its params make it incomparable, so comparing two Error values with == would panic, while pointers compare by identity.

func Blank

func Blank() *Error

Blank reports a required value left at its zero value.

func Confirmation

func Confirmation(attribute string) *Error

Confirmation reports a value that does not match the field it confirms, whose JSON name is attribute.

func EqualTo

func EqualTo[N number](count N) *Error

EqualTo reports a number other than count.

func Fail

func Fail(code string) *Error

Fail returns an Error with code.

func FailWith

func FailWith(code string, params map[string]any) *Error

FailWith is Fail with params for the message. params is copied, so the caller may go on to change it.

func GreaterThan

func GreaterThan[N number](count N) *Error

GreaterThan reports a number that is not greater than count.

func GreaterThanOrEqualTo

func GreaterThanOrEqualTo[N number](count N) *Error

GreaterThanOrEqualTo reports a number below count.

func Inclusion

func Inclusion() *Error

Inclusion reports a value outside the allowed set.

func Invalid

func Invalid() *Error

Invalid reports a value in the wrong format.

func LessThan

func LessThan[N number](count N) *Error

LessThan reports a number that is not less than count.

func LessThanOrEqualTo

func LessThanOrEqualTo[N number](count N) *Error

LessThanOrEqualTo reports a number above count.

func OtherThan

func OtherThan[N number](count N) *Error

OtherThan reports a number equal to count.

func TooLong

func TooLong(count int) *Error

TooLong reports a length above count.

func TooShort

func TooShort(count int) *Error

TooShort reports a length below count.

func WrongLength

func WrongLength(count int) *Error

WrongLength reports a length other than count.

func (*Error) Code

func (e *Error) Code() string

Code returns the code naming the broken rule.

func (*Error) Error

func (e *Error) Error() string

Error formats the code and then the params in key order, such as "validate: too_long (count=50)".

func (*Error) Params

func (e *Error) Params() map[string]any

Params returns a copy of the params, or nil when there are none.

type Errors

type Errors []FieldError

Errors collects every rule broken while validating a value, in the order they were found. A generated Validate method returns it through Err.

func (*Errors) Add

func (e *Errors) Add(field string, err error)

Add records err as broken on field. A nil err is ignored, so that the result of a rule can be passed as it is.

func (Errors) Err

func (e Errors) Err() error

Err returns e, or nil when nothing broke. Returning e itself would hand the caller a non-nil error holding nothing.

func (Errors) Error

func (e Errors) Error() string

Error lists every broken rule, such as "validate: name: blank; tags[2]: too_long (count=20)".

func (Errors) MarshalJSON

func (e Errors) MarshalJSON() ([]byte, error)

MarshalJSON is MarshalJSONTo for an encoder that knows only json.Marshaler. encoding/json itself calls MarshalJSONTo.

func (Errors) MarshalJSONTo

func (e Errors) MarshalJSONTo(enc *jsontext.Encoder) error

MarshalJSONTo encodes e as Rails encodes errors.details: an object keyed by field, in the order the fields first broke, each holding a list with the code under "error" and the params beside it, in key order.

{"name":[{"error":"blank"}],"tags[2]":[{"error":"too_long","count":20}]}

A param named "error" would hide the code, so it is left out.

func (*Errors) Nest

func (e *Errors) Nest(field string, err error)

Nest records what validating the value of field reported. The fields of Errors move under field ("address" and "zip" make "address.zip"), and an empty field leaves them where they are, as for an embedded struct. Any other error is recorded on field itself, or on Base when field is empty. A nil err is ignored.

func (Errors) Unwrap

func (e Errors) Unwrap() []error

Unwrap returns each FieldError, so that errors.As finds the rule's error behind any of them.

type FieldError

type FieldError struct {
	// Field is the path to the field, spelled with JSON names, such as
	// "address.zip" or "tags[2]"; or Base.
	Field string

	// Err is the rule's error. It is usually Coded; one that is not reports the
	// code "invalid", Rails' catch-all.
	Err error
}

FieldError is a broken rule together with the field it broke on.

func (FieldError) Code

func (e FieldError) Code() string

Code returns the code of Err, or "invalid" when Err carries none.

func (FieldError) Error

func (e FieldError) Error() string

Error formats the field followed by the code and params, such as "tags[2]: too_long (count=20)". An error that is not Coded contributes its own text instead.

func (FieldError) Params

func (e FieldError) Params() map[string]any

Params returns the params of Err, or nil when Err carries none.

func (FieldError) Path added in v0.0.5

func (e FieldError) Path() string

Path returns Field. It is the method go-kanna/i18n renders an error by, together with Code and Params, to name the field in a message.

func (FieldError) Unwrap

func (e FieldError) Unwrap() error

Unwrap returns Err.

Directories

Path Synopsis
cmd
kanna-validate command
Command kanna-validate generates Validate methods from the validate tags of structs marked //kanna:validate.
Command kanna-validate generates Validate methods from the validate tags of structs marked //kanna:validate.
internal
gen
Package gen generates Validate methods from the validate tags of structs marked //kanna:validate.
Package gen generates Validate methods from the validate tags of structs marked //kanna:validate.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL