Skip to content

Go quickstart#

Getting started with Protovalidate is simple if you're familiar with Go and Buf—otherwise, you may want to follow the step-by-step example.

  1. Add buf.build/bufbuild/protovalidate to buf.yaml then buf dep update.
  2. Add validation rules and generate code. Don't forget managed mode configuration.
    message User {
        string name = 1 [(buf.validate.field).required = true];
    }
    
  3. Install Protovalidate: go get buf.build/go/protovalidate.
  4. Validate Protobuf messages:
    err := protovalidate.Validate(message)
    
  5. Validate RPC requests with interceptors.

Step-by-step example#

Start by setting up the example project:

  1. Install the Buf CLI. If you already have, run buf --version to verify that you're using at least 1.54.0.
  2. Have git and Go installed.
  3. Clone the buf-examples repository:

    $ git clone https://github.com/bufbuild/buf-examples.git
    
  4. Open a terminal to the repository and navigate to protovalidate/quickstart-go/start.

The quickstart code contains Buf CLI configuration files (buf.yaml, buf.gen.yaml), a simple weather_service.proto, and an idiomatic unit test.

Add Protovalidate to schemas#

Depend on Protovalidate#

Protovalidate is available through the Buf Schema Registry and provides the Protobuf extensions, options, and messages powering validation.

Add it as a dependency in buf.yaml:

buf.yaml
version: v2
modules:
  - path: proto
+ deps:
+   - buf.build/bufbuild/protovalidate
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Next, update dependencies. You may see a warning that Protovalidate hasn't yet been used. That's fine.

$ buf dep update
WARN    Module buf.build/bufbuild/protovalidate is declared in your buf.yaml deps but is unused...

Add rules to a message#

Open proto/bufbuild/weather/v1/weather_service.proto, import Protovalidate, and add validation rules to GetWeatherRequest.

proto/bufbuild/weather/v1/weather_service.proto
syntax = "proto3";

package bufbuild.weather.v1;

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

// GetWeatherRequest is a request for weather at a point on Earth.
message GetWeatherRequest {
  // latitude must be between -90 and 90, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
- float latitude = 1;
+ float latitude = 1 [
+   (buf.validate.field).float.gte = -90,
+   (buf.validate.field).float.lte = 90
+ ];

  // longitude must be between -180 and 180, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
- float longitude = 2;
+ float longitude = 2 [
+   (buf.validate.field).float.gte = -180,
+   (buf.validate.field).float.lte = 180
+ ];

  // forecast_date for the weather request. It must be within the next
  // three days.
  google.protobuf.Timestamp forecast_date = 3;
}

Try it in the Playground#

Experiment with Protovalidate rules in the Protovalidate playground—modify this example, try out a custom CEL rule, or write your own validation logic without any local setup.

Lint your changes#

Some rule combinations compile successfully but fail at runtime. This example requires latitude but also skips its validation when it has its zero value, creating a logical contradiction:

Example IGNORE_IF_ZERO_VALUE lint error
message GetWeatherRequest {
  float latitude = 1 [
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).required = true,
    (buf.validate.field).float.gte = -90,
    (buf.validate.field).float.lte = 90
  ];
}

buf lint identifies these and other problems, like invalid CEL expressions, with its PROTOVALIDATE rule :

Buf lint errors for the PROTOVALIDATE rule
$ buf lint
proto/bufbuild/weather/v1/weather_service.proto:29:5:Field "latitude" has both
(buf.validate.field).required and (buf.validate.field).ignore=IGNORE_IF_ZERO_VALUE.
A field cannot be empty if it is required.

Run buf lint whenever you edit your schemas and in GitHub Actions or other CI/CD tools.

Build the module#

Now that you've added Protovalidate as a dependency, updated your schema with rules, and validated changes with buf lint, your module should build with no errors:

$ buf build

Generate code#

With Protovalidate, you don't need any new code generation plugins: its rules are compiled as part of your message descriptors.

Managed mode requirements#

Update your managed mode options in buf.gen.yaml, or your generated code won't compile:

buf.gen.yaml
version: v2
inputs:
  - directory: proto
plugins:
  - remote: buf.build/protocolbuffers/go:v1.36.5
    out: gen
    opt:
      - paths=source_relative
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: github.com/bufbuild/buf-examples/protovalidate/quickstart-go/start/gen
+ # Don't modify any file option or field option for Protovalidate. Without
+ # this, generated Go will fail to compile.
+ disable:
+   - file_option: go_package
+     module: buf.build/bufbuild/protovalidate

Run buf generate to include your new rules in the GetWeatherRequest descriptor:

$ buf generate

To learn more about generating code with the Buf CLI, read the code generation overview.

Add business logic with CEL#

Real world validation rules are often complicated and need more than a simple set of static rules:

  1. A BuyMovieTicketsRequest request must be for a showtime in the future but no more than two weeks in the future.
  2. A CreateTeamRequest with repeated members must ensure all email addresses are unique across the team.
  3. A ScheduleMeetingRequest must have a start_time before its end_time, and the meeting duration can't exceed 8 hours.

Protovalidate can meet all of these requirements because all Protovalidate rules are defined in Common Expression Language (CEL). CEL is a lightweight, high-performance expression language that allows expressions like this.first_flight_duration + this.second_flight_duration < duration('48h') to evaluate consistently across languages.

Adding a CEL-based rule to a field is straightforward. Instead of a providing a static value, you provide a unique identifier (id), an error message, and a CEL expression. Building on the prior GetWeatherRequest example, add a custom rule stating that users must ask for weather forecasts within the next 72 hours:

proto/bufbuild/weather/v1/weather_service.proto
syntax = "proto3";

package bufbuild.weather.v1;

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

// GetWeatherRequest is a request for weather at a point on Earth.
message GetWeatherRequest {
  // latitude must be between -90 and 90, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
  float latitude = 1 [
    (buf.validate.field).float.gte = -90,
    (buf.validate.field).float.lte = 90
  ];
  // longitude must be between -180 and 180, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
  float longitude = 2 [
    (buf.validate.field).float.gte = -180,
    (buf.validate.field).float.lte = 180
  ];

  // forecast_date for the weather request. It must be within the next
  // three days.
- google.protobuf.Timestamp forecast_date = 3;
+ google.protobuf.Timestamp forecast_date = 3 [(buf.validate.field).cel = {
+     id: "forecast_date.within_72_hours"
+     message: "Forecast date must be in the next 72 hours."
+     expression: "this >= now && this <= now + duration('72h')"
+ }];
}

Remember to recompile and regenerate code:

$ buf generate

Run validation#

The example code has a failing test (weather_test.go). Let's get it to pass, using Protovalidate's Go API to validate sample messages.

  1. Make sure you've navigated to protovalidate/quickstart-go/start within the buf-examples repository.

  2. Install Protovalidate using go get.

    $ go get buf.build/go/protovalidate
    
  3. Run weather/weather_test.go with go test. It should fail: it expects invalid latitudes and longitudes to be rejected, but you haven't yet added any validation.

    $ go test ./weather
    --- FAIL: TestRequests (0.00s)
        --- FAIL: TestRequests/latitude_too_low (0.00s)
            weather_test.go:65:
                    Error Trace:    /Users/janedoe/dev/bufbuild/buf-examples/protovalidate/quickstart-go/start/weather/weather_test.go:65
                    Error:          An error is expected but got nil.
                    Test:           TestRequests/latitude_too_low
        --- FAIL: TestRequests/latitude_too_high (0.00s)
            weather_test.go:65:
                    Error Trace:    /Users/janedoe/dev/bufbuild/buf-examples/protovalidate/quickstart-go/start/weather/weather_test.go:65
                    Error:          An error is expected but got nil.
                    Test:           TestRequests/latitude_too_high
    FAIL
    FAIL    github.com/bufbuild/buf-examples/protovalidate/quickstart-go/start/weather  0.244s
    FAIL
    
  4. Open weather/weather.go. Update the validateWeather function to return the result of protovalidate.Validate():

    weather/weather.go
    package weather
    
    import (
        weatherv1 "github.com/bufbuild/buf-examples/protovalidate/quickstart-go/start/gen/bufbuild/weather/v1"
    +   "buf.build/go/protovalidate"
    )
    
    - func validateWeather(_ *weatherv1.GetWeatherRequest) error {
    -     // TODO: validate the request
    -     return nil
    - }
    + func validateWeather(req *weatherv1.GetWeatherRequest) error {
    +     return protovalidate.Validate(req)
    + }
    
  5. Run go test. Now that you've added validation, all tests should pass.

    $ go test ./weather
    

You've now walked through the basic steps for using Protovalidate: adding it as a dependency, annotating your schemas with rules, and validating Protobuf messages.

Validate RPC requests#

One of Protovalidate's most common use cases is for validating requests made to RPCs. Use the Connect Go or gRPC-Go Protovalidate interceptors for automatic validation across your APIs.

Adding them is no different from configuring any other RPC interceptor:

// Create the validation interceptor provided by connectrpc.com/validate.
interceptor, err := validate.NewInterceptor()
if err != nil {
    log.Fatal(err)
}

// Include the interceptor when adding handlers.
path, handler := weatherv1connect.NewWeatherServiceHandler(
    weatherServer,
    connect.WithInterceptors(interceptor),
)
// Create a Protovalidate Validator
validator, err := protovalidate.New()
if err != nil {
    log.Fatal(err)
}

// Use the protovalidate_middleware interceptor provided by grpc-ecosystem
interceptor := protovalidate_middleware.UnaryServerInterceptor(validator)

// Include the interceptor when configuring the gRPC server.
grpcServer := grpc.NewServer(
    grpc.UnaryInterceptor(interceptor),
)

For a deep dive into using Protovalidate for RPCs, explore one of these Protovalidate integration quickstarts:

Next steps#

Read on to learn more about enabling schema-first validation with Protovalidate: