rss resume / curriculum vitae linkedin linkedin gitlab github twitter mastodon instagram
Learning Protocol Buffers: Validations
Nov 13, 2023

Disclaimer: This post includes Amazon affiliate links. Clicking on them earns me a commission and does not affect the final price.

Historically, proto-gen-validate has been the generator used to create validation rules for Protocol Buffers. However, thanks to the Buf team, a new imminent version is coming soon: V2, officially called protovalidate, and an official beta is available for testing.

In this post, I will cover this new beta and show you how to integrate it with an existing protocol buffers codebase.



The code used for this post is available on GitHub.

What is protovalidate?

protovalidate is a new generator that uses Google’s Common Expression Language (CEL) to define validation rules for the fields in your Protocol Buffer messages. It could be overwhelming initially, considering this is another thing you must learn, so take your time and read the docs before moving forward. I encourage you to start from the beginning by reading the protovalidate docs.

For this example, I’m using the existing gRPC Microservice demo I used before; specifically, we are adding validation to some of the fields of the User message.

Using protovalidate

To start using protovalidate, we need a few steps. Let’s get started.

Update buf.yaml

The first step is to update buf.yaml to include the protovalidate dependency. To do that, we edit it and add the following:

--- a/buf.yaml
+++ b/buf.yaml
@@ -5,3 +5,5 @@ breaking:
 lint:
   use:
     - DEFAULT
+deps:
+  - buf.build/bufbuild/protovalidate

Next, we run buf mod update, thus creating a new buf.lock file, this new file includes the module’s dependency manifest: protovalidate.

Update messages

The next step is to update buf.gen.yaml to properly generate the Go code after running buf generate. To do that, we add the following:

--- a/buf.gen.yaml
+++ b/buf.gen.yaml
@@ -2,6 +2,10 @@
 version: v1
 managed:
   enabled: true
+  go_package_prefix:
+    default: .
+    except:
+      - buf.build/bufbuild/protovalidate
 plugins:
   - name: go # Synonym with: protoc-gen-<name>
     out: gen/go

Adding the go_package_prefix block will require us to update all .proto files and remove the option go_package directive. We need this to allow buf to work properly and generate the Go files correctly.

Then we go and edit the User message:

--- a/user/v1/user.proto
+++ b/user/v1/user.proto
@@ -2,13 +2,13 @@ syntax = "proto3";

 package user.v1;

-option go_package = "github.com/MarioCarrion/grpc-microservice-example/gen/go/user/v1;userpb";
+import "buf/validate/validate.proto";

 message User {
   string           uuid           = 1;
-  string           full_name      = 2;
-  int64            birth_year     = 3;
-  optional uint32  salary         = 4;
+  string           full_name      = 2 [(buf.validate.field).string.min_len = 1];
+  int64            birth_year     = 3 [(buf.validate.field).int64.gt = 1900];
+  optional uint32  salary         = 4 [(buf.validate.field).uint32.gt = 0];
   repeated Address addresses      = 5;
   MaritalStatus    marital_status = 6;
 }

The two essential things to call out are the import of validate.proto and the constraint directives added to validate some of the fields:

  • The length of full_name has to be at least 1,
  • birth_year must be greater than 1900, and
  • The optional salary field has to be greater than 0.

Finally, run buf generate to generate the new Go code that supports validation.

Putting everything together

For this final step, we need to import the package bufbuild/protovalidate-go:

go get github.com/bufbuild/protovalidate-go

Then, implement the validation code to validate our message effectively:

--- /dev/null
+++ b/examples/validate/main.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+       "fmt"
+
+       "github.com/bufbuild/protovalidate-go"
+
+       userpb "github.com/MarioCarrion/grpc-microservice-example/gen/go/user/v1"
+)
+
+func main() {
+       user := &userpb.User{}
+
+       v, err := protovalidate.New()
+       if err != nil {
+               fmt.Println("failed to initialize validator:", err)
+       }
+
+       if err = v.Validate(user); err != nil {
+               fmt.Println("validation failed:", err)
+       } else {
+               fmt.Println("validation succeeded")
+       }
+}

Conclusion

Using protovalidate is an exciting approach to validating Protocol Buffers. Things get interesting when trying to determine precisely the error. Still, other than that, this is an excellent way to explicitly indicate to your customers the expected behavior of the messages in a standard mechanism that applies to any programming language.

If you’re looking to sink your teeth into more Go-related topics I recommend the following:


Back to posts