Protovalidate in gRPC and Go
This quickstart shows how to add Protovalidate to a Go RPC powered by gRPC:
- Adding the Protovalidate dependency.
- Annotating Protobuf files and regenerating code.
- Adding a gRPC interceptor.
- Testing your validation logic.
Just need an example? There's an example of Protovalidate for gRPC and Go in GitHub.
Prerequisites
Section titled “Prerequisites”-
Install the Buf CLI. If you already have, run
buf --versionto verify that you're using at least1.54.0. -
Clone the
buf-examplesrepo and navigate to theprotovalidate/grpc-go/startdirectory:Terminal window $ git clone https://github.com/bufbuild/buf-examples.git && cd buf-examples/protovalidate/grpc-go/start
This quickstart's CreateInvoice RPC doesn't have any input validation. Your goal is to pass a unit test verifying that you've added two validation rules using Protovalidate:
- Requests must provide an
Invoicewith a UUIDinvoice_id. - Within the
Invoicemessage, all of itsrepeated LineItem line_itemsmust have unique combinations ofproduct_idandunit_price.
Run the test now, and you can see that it fails:
$ go test -v ./...--- PASS: TestCreateInvoice/A_valid_invoice_passes_validation (0.00s)--- FAIL: TestCreateInvoice/InvoiceId_is_required (0.00s)--- FAIL: TestCreateInvoice/Two_line_items_cannot_have_the_same_product_id_and_unit_price (0.00s)When this test passes, you've met your goal.
Run the server
Section titled “Run the server”Before you begin to code, verify that the example is working. Compile and start the included server:
$ go run cmd/server.goAfter a few seconds, you should see that it has started:
2025/02/04 19:52:02 INFO starting invoice service address=:50051In a second terminal window, use buf curl to send an invalid CreateInvoiceRequest:
$ buf curl \ --data '{ "invoice": { "invoice_id": "" } }' \ --protocol grpc \ --http2-prior-knowledge \ http://localhost:50051/invoice.v1.InvoiceService/CreateInvoiceThe server should respond with the version number of the invoice that was created, despite the invalid request. That's what you're here to fix.
{ "version": "1"}Before you start coding, take a few minutes to explore the code in the example.
Explore quickstart code
Section titled “Explore quickstart code”This quickstart uses the example in grpc-go/start. All filenames are relative to this directory.
Protobuf
Section titled “Protobuf”The project provides a single unary RPC:
// InvoiceService is a simple CRUD service for managing invoices.service InvoiceService { // CreateInvoice creates a new invoice. rpc CreateInvoice(CreateInvoiceRequest) returns (CreateInvoiceResponse);}CreateInvoiceRequest includes an invoice field that's an Invoice message. An Invoice has a repeated field of type LineItem:
message Invoice { // invoice_id is a unique identifier for this invoice. string invoice_id = 1; // line_items represent individual items on this invoice. repeated LineItem line_items = 4;}
// LineItem is an individual good or service added to an invoice.message LineItem { // product_id is the unique identifier for the good or service on this line. string product_id = 2;
// quantity is the unit count of the good or service provided. uint64 quantity = 3;}When you add Protovalidate, you'll update the following files:
buf.yaml: Protovalidate must be added as a dependency.buf.gen.yaml: To avoid a common Go issue in projects using the Buf CLI's managed mode, you'll see how to exclude Protovalidate from package renaming.
You'll be working in cmd/server.go. It's an executable that runs a server on port 50051. You'll edit it to add a Protovalidate interceptor to gRPC.
internal/invoice/invoice.go provides invoice.Service, a handler for the InvoiceService. Its CreateInvoice function sends back a static response.
Now that you know your way around the example code, it's time to integrate Protovalidate.
Integrate Protovalidate
Section titled “Integrate Protovalidate”It's time to add Protovalidate to your project. It may be useful to read the Protovalidate overview and its quickstart before continuing.
Add Protovalidate dependency
Section titled “Add Protovalidate dependency”Because Protovalidate is a publicly available Buf Schema Registry (BSR) module, it's simple to add it to any Buf CLI project.
-
Add Protovalidate to your Go project:
Terminal window $ go get buf.build/go/protovalidate -
Add Protovalidate as a dependency to
buf.yaml.buf.yaml # For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yamlversion: v2modules:- path: protodeps:- buf.build/bufbuild/protovalidatelint:use:- STANDARDbreaking:use:- FILE -
Update dependencies with the Buf CLI. You'll be warned that Protovalidate is declared but unused. That's fine.
Updating CLI dependencies $ buf dep updateWARN Module buf.build/bufbuild/protovalidate is declared in your buf.yaml deps but is unused... -
Because this example uses managed mode, exclude Protovalidate from any updates to
go_package.buf.gen.yaml version: v2inputs:- directory: protoplugins:- remote: buf.build/protocolbuffers/goout: genopt: paths=source_relative- remote: buf.build/grpc/goout: genopt: paths=source_relativemanaged:enabled: trueoverride:- file_option: go_package_prefixvalue: github.com/bufbuild/buf-examples/protovalidate/grpc-go/start/gendisable:- file_option: go_packagemodule: buf.build/bufbuild/protovalidate -
Verify that configuration is complete by running
buf generate. It should complete with no error.
Add a standard rule
Section titled “Add a standard rule”You'll now add a standard rule to proto/invoice.proto to require that the invoice_id field is a UUID. Start by importing Protovalidate:
syntax = "proto3";
package invoice.v1;
import "buf/validate/validate.proto";import "google/protobuf/timestamp.proto";You could use the required rule to verify that requests provide this field, but Protovalidate makes it easy to do more specific validations.
Use string.uuid to declare that invoice_id must be present and a valid UUID.
// Invoice is a collection of goods or services sold to a customer.message Invoice { // invoice_id is a unique identifier for this invoice. string invoice_id = 1; string invoice_id = 1 [ (buf.validate.field).string.uuid = true ];
// account_id is the unique identifier for the account purchasing goods. string account_id = 2;
// invoice_date is the date for an invoice. It should represent a date and // have no values for time components. google.protobuf.Timestamp invoice_date = 3;
// line_items represent individual items on this invoice. repeated LineItem line_items = 4;}Learn more about string and standard rules.
Enforce complex rules
Section titled “Enforce complex rules”In Invoice, the line_items field needs to meet two business rules:
- There should always be at least one
LineItem. - No two
LineItemsshould ever share the sameproduct_idandunit_price.
Protovalidate can enforce both of these rules by combining a standard rule with a custom rule written in Common Expression Language (CEL).
First, use the min_items standard rule to require at least one LineItem:
// Invoice is a collection of goods or services sold to a customer.message Invoice { // invoice_id is a unique identifier for this invoice. string invoice_id = 1 [ (buf.validate.field).string.uuid = true ];
// account_id is the unique identifier for the account purchasing goods. string account_id = 2;
// invoice_date is the date for an invoice. It should represent a date and // have no values for time components. google.protobuf.Timestamp invoice_date = 3;
// line_items represent individual items on this invoice. repeated LineItem line_items = 4; repeated LineItem line_items = 4 [ (buf.validate.field).repeated.min_items = 1 ];}Next, use a CEL expression to add a custom rule. Use the map, string, and unique CEL functions to check that no combination of product_id and unit_price appears twice within the array of LineItems:
// Invoice is a collection of goods or services sold to a customer.message Invoice { // invoice_id is a unique identifier for this invoice. string invoice_id = 1 [ (buf.validate.field).string.uuid = true ];
// account_id is the unique identifier for the account purchasing goods. string account_id = 2;
// invoice_date is the date for an invoice. It should represent a date and // have no values for time components. google.protobuf.Timestamp invoice_date = 3;
// line_items represent individual items on this invoice. repeated LineItem line_items = 4 [ (buf.validate.field).repeated.min_items = 1 (buf.validate.field).repeated.min_items = 1,
(buf.validate.field).cel = { id: "line_items.logically_unique" message: "line items must be unique combinations of product_id and unit_price" expression: "this.map( it, it.product_id + '-' + string(it.unit_price) ).unique()" } ];}You've added validation rules to your Protobuf. To enforce them, you still need to regenerate code and add a Protovalidate interceptor to your server.
Learn more about custom rules.
Compile Protobuf and Go
Section titled “Compile Protobuf and Go”Next, compile your Protobuf and regenerate code, adding the Protovalidate options to all of your message descriptors:
$ buf generateIf you're still running the server, stop it with Ctrl-c. Compile the new generated code and restart:
$ go run cmd/server.goAfter a few seconds, you should see that it has started:
2025/02/05 06:51:44 INFO starting invoice service address=:50051In a second terminal window, use buf curl to send the same invalid CreateInvoiceRequest:
$ buf curl \ --data '{ "invoice": { "invoice_id": "" } }' \ --protocol grpc \ --http2-prior-knowledge \ http://localhost:50051/invoice.v1.InvoiceService/CreateInvoiceThe response may be a surprise: the server still considers the request valid and returns a version number for the new invoice:
{ "version": "1"}The RPC is still successful because no Connect or gRPC implementations automatically enforce Protovalidate rules. To enforce your validation rules, you'll need to add an interceptor.
Add a Protovalidate interceptor
Section titled “Add a Protovalidate interceptor”Thanks to the gRPC Ecosystem project's prebuilt Protovalidate interceptor, adding a Protovalidate interceptor for gRPC and Go is just a few lines of code.
Follow these steps to begin enforcing Protovalidate rules:
-
In your first console window, use
Ctrl-cto stop your server. -
Install the interceptor.
Terminal window $ go get github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate@v2.3.2 -
Import
protovalidateandprotovalidate_middlewareincmd/server.go.cmd/server.go import ("buf.build/go/protovalidate""context""errors""fmt""github.com/bufbuild/protovalidate-go"protovalidate_middleware "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/protovalidate""log/slog""net""os""os/signal""github.com/bufbuild/buf-examples/protovalidate/grpc-go/finish/gen/invoice/v1""github.com/bufbuild/buf-examples/protovalidate/grpc-go/finish/internal""golang.org/x/sync/errgroup""google.golang.org/grpc""google.golang.org/grpc/reflection") -
Add the interceptor when creating a gRPC
Server:cmd/server.go func run(ctx context.Context) error {validator, err := protovalidate.New()if err != nil {return errors.New("failed to create protovalidate validator")}grpcServer := grpc.NewServer()grpcServer := grpc.NewServer(grpc.UnaryInterceptor(protovalidate_middleware.UnaryServerInterceptor(validator)),)reflection.Register(grpcServer)invoicev1.RegisterInvoiceServiceServer(grpcServer, invoice.NewService())// Code omitted for brevity} -
Stop (
Ctrl-c) and restart your server:Terminal window $ go run cmd/server.goAfter a few seconds, you should see that it has started:
Terminal window 2025/02/03 15:30:15 INFO starting invoice server addr=localhost:8080
Now that you've added the Protovalidate interceptor and restarted your server, try the buf curl command again:
$ buf curl \ --data '{ "invoice": { "invoice_id": "" } }' \ --protocol grpc \ --http2-prior-knowledge \ http://localhost:50051/invoice.v1.InvoiceService/CreateInvoiceThis time, you should receive a block of JSON representing Protovalidate's enforcement of your rules. In the abridged excerpt below, you can see that it contains details about every field that violated Protovalidate rules:
{ "violations": [ { "ruleId": "string.uuid_empty", "message": "value is empty, which is not a valid UUID" }, { "ruleId": "repeated.min_items", "message": "value must contain at least 1 item(s)" } ]}Last, use buf curl to test the custom rule that checks for logically unique LineItems:
$ buf curl \ --data '{ "invoice": { "invoice_id": "079a91c2-cb8b-4f01-9cf9-1b9c0abdd6d2", "line_items": [{"product_id": "A", "unit_price": "1" }, {"product_id": "A", "unit_price": "1" }] } }' \ --protocol grpc \ --http2-prior-knowledge \ http://localhost:50051/invoice.v1.InvoiceService/CreateInvoiceYou can see that this more complex expression is enforced at runtime:
{ "violations": [ { "ruleId": "line_items.logically_unique", "message": "line items must be unique combinations of product_id and unit_price" } ]}You've now added Protovalidate to a gRPC in Go, but buf curl isn't a great way to make sure you're meeting all of your requirements.
Next, you'll see one way to verify Protovalidate rules in tests.
Test Protovalidate errors
Section titled “Test Protovalidate errors”The starting code for this quickstart contains a table test in internal/invoice/invoice_test.go. It starts a server with a Protovalidate interceptor and iterates through a series of test cases.
In the prior section, you saw that the violations list returned by Protovalidate follows a predictable structure. Each violation in the list is a Protobuf message named Violation, defined within Protovalidate itself.
The test already provides a convenient way to declare expected violations through a violationSpec type:
// violationSpec is a simple representation of fields tested when inspecting// a connect.Error that we expect to contain Protovalidate validate.Violations// messages.type violationSpec struct { ruleID string fieldPath string message string}Examine the highlighted lines in invoice_test.go, noting that the tests check for specific expected violations:
func TestCreateInvoice(t *testing.T) { // Code omitted for brevity...
testCases := map[string]struct { producer func(invoice *invoicev1.Invoice) *invoicev1.Invoice violations []violationSpec }{ "An invoice can be created": { producer: func(invoice *invoicev1.Invoice) *invoicev1.Invoice { return invoice }, }, "InvoiceId is required": { producer: func(invoice *invoicev1.Invoice) *invoicev1.Invoice { invoice.InvoiceId = "" return invoice }, violations: []violationSpec{ { ruleID: "string.uuid_empty", fieldPath: "invoice.invoice_id", message: "value is empty, which is not a valid UUID", }, }, }, "Two line items cannot have the same product_id and unit price": { producer: func(invoice *invoicev1.Invoice) *invoicev1.Invoice { invoice.GetLineItems()[0].ProductId = invoice.GetLineItems()[1].GetProductId() invoice.GetLineItems()[0].UnitPrice = invoice.GetLineItems()[1].GetUnitPrice() return invoice }, violations: []violationSpec{ { ruleID: "line_items.logically_unique", fieldPath: "invoice.line_items", message: "line items must be unique combinations of product_id and unit_price", }, }, }, } // Code omitted for brevity}To check your work, run all tests with the -v flag.
$ go test -v ./...If all tests pass, you've met your goal:
--- PASS: TestCreateInvoice (0.00s) --- PASS: TestCreateInvoice/InvoiceId_is_required (0.01s) --- PASS: TestCreateInvoice/InvoiceId_must_be_a_UUID (0.00s) --- PASS: TestCreateInvoice/Two_line_items_cannot_have_the_same_identifier (0.01s)PASSok github.com/bufbuild/buf-examples/protovalidate/grpc-go/finish/internal 0.479sMore testing examples
Section titled “More testing examples”The finish directory contains a thorough test that you can use as an example for your own tests. Its invoice.proto file also contains extensive Protovalidate rules.
Wrapping up
Section titled “Wrapping up”In this quickstart, you've learned the basics of working with Protovalidate:
- Adding Protovalidate to your project.
- Declaring validation rules in your Protobuf files.
- Enabling their enforcement within an RPC API.
- Testing their functionality.
Further reading
Section titled “Further reading”- Add Protovalidate's standard rules to schemas
- Use CEL expressions to declare field and message-level custom rules
- Reuse logic with predefined rules
- Learn more about Protovalidate's relationship with CEL.