protovalidate

package module
v0.9.2 Latest Latest
Warning

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

Go to latest
Published: Feb 12, 2025 License: Apache-2.0 Imports: 18 Imported by: 211

README

The Buf logo protovalidate-go

CI Conformance Report Card GoDoc BSR

protovalidate-go is the Go language implementation of protovalidate designed to validate Protobuf messages at runtime based on user-defined validation constraints. Powered by Google's Common Expression Language (CEL), it provides a flexible and efficient foundation for defining and evaluating custom validation rules. The primary goal of protovalidate is to help developers ensure data consistency and integrity across the network without requiring generated code.

The protovalidate project

Head over to the core protovalidate repository for:

Other protovalidate runtime implementations:

And others coming soon:

  • TypeScript: protovalidate-ts

For Connect see connectrpc/validate-go.

Installation

To install the package, use the go get command from within your Go module:

go get github.com/bufbuild/protovalidate-go

Import the package into your Go project:

import "github.com/bufbuild/protovalidate-go"

Remember to always check for the latest version of protovalidate-go on the project's GitHub releases page to ensure you're using the most up-to-date version.

Usage

Implementing validation constraints

Validation constraints are defined directly within .proto files. Documentation for adding constraints can be found in the protovalidate project README and its comprehensive docs.

syntax = "proto3";

package my.package;

import "google/protobuf/timestamp.proto";
import "buf/validate/validate.proto";

message Transaction {
  uint64 id = 1 [(buf.validate.field).uint64.gt = 999];
  google.protobuf.Timestamp purchase_date = 2;
  google.protobuf.Timestamp delivery_date = 3;

  string price = 4 [(buf.validate.field).cel = {
    id: "transaction.price",
    message: "price must be positive and include a valid currency symbol ($ or £)",
    expression: "(this.startsWith('$') || this.startsWith('£')) && double(this.substring(1)) > 0"
  }];

  option (buf.validate.message).cel = {
    id: "transaction.delivery_date",
    message: "delivery date must be after purchase date",
    expression: "this.delivery_date > this.purchase_date"
  };
}
Buf managed mode

protovalidate-go assumes the constraint extensions are imported into the generated code via buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go.

If you are using Buf managed mode to augment Go code generation, ensure that the protovalidate module is excluded in your buf.gen.yaml:

buf.gen.yaml v1

version: v1
# <snip>
managed:
  enabled: true
  go_package_prefix:
    except:
      - buf.build/bufbuild/protovalidate
# <snip>

buf.gen.yaml v2

version: v2
# <snip>
managed:
  enabled: true
  disable:
    - file_option: go_package_prefix
      module: buf.build/bufbuild/protovalidate
# <snip>
Example
package main

import (
	"fmt"
	"time"

	pb "github.com/path/to/generated/protos"
	"github.com/bufbuild/protovalidate-go"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	msg := &pb.Transaction{
		Id:           1234,
		Price:        "$5.67",
		PurchaseDate: timestamppb.New(time.Now()),
		DeliveryDate: timestamppb.New(time.Now().Add(time.Hour)),
	}
	if err = protovalidate.Validate(msg); err != nil {
		fmt.Println("validation failed:", err)
	} else {
		fmt.Println("validation succeeded")
	}
}
Lazy mode

protovalidate-go defaults to lazily construct validation logic for Protobuf message types the first time they are encountered. A validator's internal cache can be pre-warmed with the WithMessages or WithDescriptors options during initialization:

validator, err := protovalidate.New(
  protovalidate.WithMessages(
    &pb.MyFoo{},
    &pb.MyBar{},
  ),
)

Lazy mode uses a copy on write cache stategy to reduce the required locking. While performance is sub-microsecond, the overhead can be further reduced by disabling lazy mode with the WithDisableLazy option. Note that all expected messages must be provided during initialization of the validator:

validator, err := protovalidate.New(
  protovalidate.WithDisableLazy(true),
  protovalidate.WithMessages(
    &pb.MyFoo{},
    &pb.MyBar{},
  ),
)
Legacy protoc-gen-validate constraints

protoc-gen-validate code generation is not used by protovalidate-go. A migration tool is available to upgrade legacy constraints in .proto files.

Performance

Benchmarks are provided to test a variety of use-cases. Generally, after the initial cold start, validation on a message is sub-microsecond and only allocates in the event of a validation error.

[circa 14 September 2023]
goos: darwin
goarch: arm64
pkg: github.com/bufbuild/protovalidate-go
BenchmarkValidator
BenchmarkValidator/ColdStart-10              4192  246278 ns/op  437698 B/op  5955 allocs/op
BenchmarkValidator/Lazy/Valid-10         11816635   95.08 ns/op       0 B/op     0 allocs/op
BenchmarkValidator/Lazy/Invalid-10        2983478   380.5 ns/op     649 B/op    15 allocs/op
BenchmarkValidator/Lazy/FailFast-10      12268683   98.22 ns/op     168 B/op     3 allocs/op
BenchmarkValidator/PreWarmed/Valid-10    12209587   90.36 ns/op       0 B/op     0 allocs/op
BenchmarkValidator/PreWarmed/Invalid-10   3098940   394.1 ns/op     649 B/op    15 allocs/op
BenchmarkValidator/PreWarmed/FailFast-10 12291523   99.27 ns/op     168 B/op     3 allocs/op
PASS

Ecosystem

Offered under the Apache 2 license.

Documentation

Overview

Example
person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Buf Build",
	Home: &pb.Coordinates{
		Lat: 27.380583333333334,
		Lng: 33.631838888888886,
	},
}

err := Validate(person)
fmt.Println("valid:", err)

person.Email = "not an email"
err = Validate(person)
fmt.Println("invalid:", err)
Output:

valid: <nil>
invalid: validation error:
 - email: value must be a valid email address [string.email]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func FieldPathString added in v0.8.0

func FieldPathString(path *validate.FieldPath) string

FieldPathString takes a FieldPath and encodes it to a string-based dotted field path.

func Validate added in v0.7.2

func Validate(msg proto.Message) error

Validate uses a global instance of Validator constructed with no ValidatorOptions and calls its Validate function. For the vast majority of validation cases, using this global function is safe and acceptable. If you need to provide i.e. a custom ExtensionTypeResolver, you'll need to construct a Validator.

Types

type CompilationError

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

A CompilationError is returned if a CEL expression cannot be compiled & type-checked or if invalid standard constraints are applied.

func (*CompilationError) Error added in v0.9.0

func (err *CompilationError) Error() string

func (*CompilationError) Unwrap added in v0.9.0

func (err *CompilationError) Unwrap() error

type RuntimeError

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

A RuntimeError is returned if a valid CEL expression evaluation is terminated. The two built-in reasons are 'no_matching_overload' when a CEL function has no overload for the types of the arguments or 'no_such_field' when a map or message does not contain the desired field.

func (*RuntimeError) Error added in v0.9.0

func (err *RuntimeError) Error() string

func (*RuntimeError) Unwrap added in v0.9.0

func (err *RuntimeError) Unwrap() error

type ValidationError

type ValidationError struct {
	Violations []*Violation
}

A ValidationError is returned if one or more constraint violations were detected.

Example
validator, err := New()
if err != nil {
	log.Fatal(err)
}

loc := &pb.Coordinates{Lat: 999.999}
err = validator.Validate(loc)
var valErr *ValidationError
if ok := errors.As(err, &valErr); ok {
	violation := valErr.Violations[0]
	fmt.Println(violation.Proto.GetField().GetElements()[0].GetFieldName(), violation.Proto.GetConstraintId())
	fmt.Println(violation.RuleValue, violation.FieldValue)
}
Output:

lat double.gte_lte
-90 999.999
Example (Localized)
validator, err := New()
if err != nil {
	log.Fatal(err)
}

type ErrorInfo struct {
	FieldName  string
	RuleValue  any
	FieldValue any
}

var ruleMessages = map[string]string{
	"string.email_empty": "{{.FieldName}}: メールアドレスは空であってはなりません。\n",
	"string.pattern":     "{{.FieldName}}: 値はパターン「{{.RuleValue}}」一致する必要があります。\n",
	"uint64.gt":          "{{.FieldName}}: 値は{{.RuleValue}}を超える必要があります。(価値:{{.FieldValue}})\n",
}

loc := &pb.Person{Id: 900}
err = validator.Validate(loc)
var valErr *ValidationError
if ok := errors.As(err, &valErr); ok {
	for _, violation := range valErr.Violations {
		_ = template.
			Must(template.New("").Parse(ruleMessages[violation.Proto.GetConstraintId()])).
			Execute(os.Stdout, ErrorInfo{
				FieldName:  violation.Proto.GetField().GetElements()[0].GetFieldName(),
				RuleValue:  violation.RuleValue.Interface(),
				FieldValue: violation.FieldValue.Interface(),
			})
	}
}
Output:

id: 値は999を超える必要があります。(価値:900)
email: メールアドレスは空であってはなりません。
name: 値はパターン「^[[:alpha:]]+( [[:alpha:]]+)*$」一致する必要があります。

func (*ValidationError) Error added in v0.9.0

func (err *ValidationError) Error() string

func (*ValidationError) ToProto added in v0.9.0

func (err *ValidationError) ToProto() *validate.Violations

ToProto converts this error into its proto.Message form.

type Validator

type Validator interface {
	// Validate checks that message satisfies its constraints. Constraints are
	// defined within the Protobuf file as options from the buf.validate
	// package. An error is returned if the constraints are violated
	// (ValidationError), the evaluation logic for the message cannot be built
	// (CompilationError), or there is a type error when attempting to evaluate
	// a CEL expression associated with the message (RuntimeError).
	Validate(msg proto.Message) error
}

func New

func New(options ...ValidatorOption) (Validator, error)

New creates a Validator with the given options. An error may occur in setting up the CEL execution environment if the configuration is invalid. See the individual ValidatorOption for how they impact the fallibility of New.

type ValidatorOption

type ValidatorOption func(*config)

A ValidatorOption modifies the default configuration of a Validator. See the individual options for their defaults and affects on the fallibility of configuring a Validator.

func WithAllowUnknownFields added in v0.7.0

func WithAllowUnknownFields() ValidatorOption

WithAllowUnknownFields specifies if the presence of unknown field constraints should cause compilation to fail with an error. When set to false, an unknown field will simply be ignored, which will cause constraints to silently not be applied. This condition may occur if a predefined constraint definition isn't present in the extension type resolver, or when passing dynamic messages with standard constraints defined in a newer version of protovalidate. The default value is false, to prevent silently-incorrect validation from occurring.

func WithDisableLazy

func WithDisableLazy() ValidatorOption

WithDisableLazy prevents the Validator from lazily building validation logic for a message it has not encountered before. Disabling lazy logic additionally eliminates any internal locking as the validator becomes read-only.

Note: All expected messages must be provided by WithMessages or WithMessageDescriptors during initialization.

Example
person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Buf Build",
	Home: &pb.Coordinates{
		Lat: 27.380583333333334,
		Lng: 33.631838888888886,
	},
}

validator, err := New(
	WithMessages(&pb.Coordinates{}),
	WithDisableLazy(),
)
if err != nil {
	log.Fatal(err)
}

err = validator.Validate(person.GetHome())
fmt.Println("person.Home:", err)
err = validator.Validate(person)
fmt.Println("person:", err)
Output:

person.Home: <nil>
person: compilation error: no evaluator available for tests.example.v1.Person

func WithExtensionTypeResolver added in v0.7.0

func WithExtensionTypeResolver(extensionTypeResolver protoregistry.ExtensionTypeResolver) ValidatorOption

WithExtensionTypeResolver specifies a resolver to use when reparsing unknown extension types. When dealing with dynamic file descriptor sets, passing this option will allow extensions to be resolved using a custom resolver.

To ignore unknown extension fields, use the WithAllowUnknownFields option. Note that this may result in messages being treated as valid even though not all constraints are being applied.

func WithFailFast

func WithFailFast() ValidatorOption

WithFailFast specifies whether validation should fail on the first constraint violation encountered or if all violations should be accumulated. By default, all violations are accumulated.

Example
loc := &pb.Coordinates{Lat: 999.999, Lng: -999.999}

validator, err := New()
if err != nil {
	log.Fatal(err)
}
err = validator.Validate(loc)
fmt.Println("default:", err)

validator, err = New(WithFailFast())
if err != nil {
	log.Fatal(err)
}
err = validator.Validate(loc)
fmt.Println("fail fast:", err)
Output:

default: validation error:
 - lat: value must be greater than or equal to -90 and less than or equal to 90 [double.gte_lte]
 - lng: value must be greater than or equal to -180 and less than or equal to 180 [double.gte_lte]
fail fast: validation error:
 - lat: value must be greater than or equal to -90 and less than or equal to 90 [double.gte_lte]

func WithMessageDescriptors added in v0.9.0

func WithMessageDescriptors(descriptors ...protoreflect.MessageDescriptor) ValidatorOption

WithMessageDescriptors allows warming up the Validator with message descriptors that are expected to be validated. Messages included transitively (i.e., fields with message values) are automatically handled.

Example
pbType, err := protoregistry.GlobalTypes.FindMessageByName("tests.example.v1.Person")
if err != nil {
	log.Fatal(err)
}

validator, err := New(
	WithMessageDescriptors(
		pbType.Descriptor(),
	),
)
if err != nil {
	log.Fatal(err)
}

person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Protocol Buffer",
}
err = validator.Validate(person)
fmt.Println(err)
Output:

<nil>

func WithMessages

func WithMessages(messages ...proto.Message) ValidatorOption

WithMessages allows warming up the Validator with messages that are expected to be validated. Messages included transitively (i.e., fields with message values) are automatically handled.

Example
validator, err := New(
	WithMessages(&pb.Person{}),
)
if err != nil {
	log.Fatal(err)
}

person := &pb.Person{
	Id:    1234,
	Email: "protovalidate@buf.build",
	Name:  "Protocol Buffer",
}
err = validator.Validate(person)
fmt.Println(err)
Output:

<nil>

type Violation added in v0.8.0

type Violation struct {
	// Proto contains the violation's proto.Message form.
	Proto *validate.Violation

	// FieldValue contains the value of the specific field that failed
	// validation. If there was no value, this will contain an invalid value.
	FieldValue protoreflect.Value

	// FieldDescriptor contains the field descriptor corresponding to the
	// field that failed validation.
	FieldDescriptor protoreflect.FieldDescriptor

	// RuleValue contains the value of the rule that specified the failed
	// constraint. Not all constraints have a value; only standard and
	// predefined constraints have rule values. In violations caused by other
	// kinds of constraints, like custom contraints, this will contain an
	// invalid value.
	RuleValue protoreflect.Value

	// RuleDescriptor contains the field descriptor corresponding to the
	// rule that failed validation.
	RuleDescriptor protoreflect.FieldDescriptor
}

Violation represents a single instance where a validation rule was not met. It provides information about the field that caused the violation, the specific unfulfilled constraint, and a human-readable error message.

Jump to

Keyboard shortcuts

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