Protovalidate moves validation rules into the .proto file
Protocol Buffer Validation - Go, Java, Python, C++ and JS/TS
At a glance
- What is it?
- Protovalidate is the semantic validation library for Protobuf, using standard annotations on your messages plus CEL expressions for anything the standard rules cannot express. Five language implementations live in separate repositories and share a conformance suite, which is the part that makes the split viable.
- Who is it for?
- Adopt Protovalidate for a Protobuf service where several languages on both sides of the wire need identical validation, since one .proto file plus one CEL expression set replaces per-language validator code. Do not adopt it for a single-language service with a handful of messages, where the generated-code approach you already have is cheaper.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Star history
- bufbuild/protovalidate1,581 ★ · +2
| Repository | 2026-10-16 | 2026-10-23 |
|---|---|---|
| bufbuild/protovalidate | 1.6k | 1.6k |
2 Stars in the last 7 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Two ways to write a rule
The README's example message shows both mechanisms in a single definition. The standard half uses field options from the imported `buf/validate/validate.proto`:
message User {
string id = 1 [(buf.validate.field).string.uuid = true];
uint32 age = 2 [(buf.validate.field).uint32.lte = 150]; // We can only hope.
string email = 3 [(buf.validate.field).string.email = true];
string first_name = 4 [(buf.validate.field).string.max_len = 64];
string last_name = 5 [(buf.validate.field).string.max_len = 64];UUID format, an upper bound on age, email format and a maximum string length, all declared as options rather than as code. That is the design in one block: the constraint travels with the schema.
The custom half uses a message-level CEL rule for anything the standard annotations cannot express:
option (buf.validate.message).cel = {
id: "first_name_requires_last_name"
message: "last_name must be present if first_name is present"
expression: "!has(this.first_name) || has(this.last_name)"
};
}The `id` is a stable machine-readable name for the rule, `message` is the text a human sees, and `expression` is CEL evaluated against the message. Note the presence check: `has()` is what distinguishes a field that is absent from one set to its zero value, which is a distinction plain field rules cannot make.
Because CEL is a real expression language rather than a template, the range of expressible constraints is much wider than a fixed list of annotations.
One repository per language, one conformance suite
The implementation is not in this repository. This one holds the specification, the schema of the annotations, the tooling and the tests, and the runtime libraries live elsewhere: `protovalidate-go`, `protovalidate-java`, `protovalidate-py`, `protovalidate-cc` and `protovalidate-es` for TypeScript and JavaScript.
That split is the interesting engineering decision, because five independently written implementations of one specification will drift unless something forces them to agree. Protovalidate's answer is conformance testing, and the README links utilities for acceptance testing of implementations at docs/conformance.md.
So the conformance suite is not a nice-to-have. It is the mechanism that lets the specification evolve: when a CEL feature or a standard rule is added, every language implementation has a shared test to pass before the behaviour counts as specified.
The Buf packaging story is visible in the repository too. There is a `buf.yaml` and a `buf.gen.yaml`, the module is published to the Buf Schema Registry, and `examples/buf.lock` and `examples/buf.yaml` show how a consumer pins it.
Documentation is per-language and per-transport: the quickstart page plus RPC quickstarts for Connect and Go, gRPC with Go, Java and Python, and a migration guide for people coming from protoc-gen-validate.
Building it is four make targets with pinned tool versions
The Makefile is small enough to read as documentation of what this project considers a complete check. The default goal is:
all: generate lint test conformanceGenerate, lint, test, conformance. The `lint` target is two halves, one for Go and one for the protos, and the proto half is where the interesting line sits:
lint-proto:
$(BIN)/buf lint
$(BIN)/buf breaking --against '.git#branch=main'
$(MAKE) lint-protovalidate`buf breaking` compares your working tree against the main branch and fails on breaking schema changes. For a project whose whole premise is that the .proto file is the contract, enforcing compatibility of that contract in CI is the point rather than an extra.
The third line, `lint-protovalidate`, checks invariants of validate.proto itself through a Go program, so the schema of the annotations is held to the same standard as the schemas using it.
Tool versions are pinned as constants at the top of the Makefile: `BUF_VERSION := v1.72.0`, `GOLANGCI_LINT_VERSION := v2.13.1` and `BAZELISK_VERSION := v1.27.0`, with the binaries fetched into `.tmp/bin`. Tests run in two modes, Go tests with the race detector and coverage, and a full Bazel test pass.
There is also `RELEASING.md` at the root, plus `.golangci.yml` for the Go lint configuration.
The examples directory is a CEL reference
The `examples/` directory is the most useful thing in the repository for learning the expression language, and it is organised as one file per CEL feature: `cel_bytes_concatenation.proto`, `cel_bytes_contains.proto`, `cel_bytes_starts_with_ends_with.proto`, `cel_field_presence.proto`, `cel_field_presence_nested.proto`, `cel_field_mask.proto`, `cel_field_map.proto`, `cel_field_repeated.proto`, `cel_map_all.proto`, `cel_map_exists.proto`, `cel_map_exists_one.proto`, `cel_map_size.proto`, `cel_duration_arithmetic.proto`, `cel_duration_from_string.proto`, `cel_conditional_operator.proto`, `cel_enum_comparison.proto`, `cel_field_selection.proto`, `cel_infinity.proto`, `cel_list_concatenation.proto` and `cel_assert_value_is_in_a_list.proto`.
Read as a set, they answer the questions you hit in the first hour. How do I know a field was set rather than defaulted? What is the difference between a repeated field and a map in an expression? How do I compare an enum? How do I do arithmetic on a duration? What happens with infinity?
There is a BUILD.bazel and a README alongside them, and the files are Bazel-built like everything else in the repository, which tells you the project treats the examples as tested artifacts rather than documentation that drifts.
What it costs to move, and when not to
The migration story is the honest part of the evaluation. The README calls Protovalidate the next generation of protoc-gen-validate and ships a dedicated migration guide, which tells you the two are not annotation-compatible and the mapping is manual.
There is a second, quieter cost: validation now runs as CEL evaluation rather than as compiled Go, and the practical question is what that costs on a hot path. The repository contains a `benchmarks/` directory at the root of this project, but no figures are published in the README, so the honest answer is that this needs measuring in your own service rather than taking from documentation.
And the fit is narrower than the schema suggests. If your service is Go on both ends with a handful of messages, the generated validators you already have are faster to keep than a CEL dependency is to adopt. The case for Protovalidate gets strong when several languages validate the same messages, when the constraints need to be reviewable in one place, or when the rule set outgrows what annotations can express.
The licence is Apache 2.0 with a LICENSE file at the root and a `NOTICE`-style copyright header managed through `COPYRIGHT_YEARS:= 2023-2026` in the Makefile. The last push was on 2026-10-12 and the recent tags are v1.2.0 on 2026-04-15, then v1.2.1 and v1.2.2 both on 2026-07-09.
Editorial conclusion
Adopt Protovalidate for a Protobuf service where several languages on both sides of the wire need identical validation, since one .proto file plus one CEL expression set replaces per-language validator code. Do not adopt it for a single-language service with a handful of messages, where the generated-code approach you already have is cheaper. Verify first that the CEL implementation you depend on behaves identically in your language, by running the conformance utilities described in docs/conformance.md, and read the protoc-gen-validate migration guide before assuming the annotation syntax carries over unchanged.
Frequently asked questions
What is Protovalidate?
It is the semantic validation library for Protobuf, providing standard annotations for common rules on messages and fields plus the ability to write custom rules in CEL. The README describes it as the next generation of protoc-gen-validate.
Which languages does Protovalidate support?
Five separate implementations: protovalidate-go, protovalidate-java, protovalidate-py, protovalidate-cc for C++, and protovalidate-es for TypeScript and JavaScript. Each lives in its own repository.
How do I write a custom validation rule?
With a message-level CEL option, giving a rule id, a human-readable message and an expression, for example an expression of `!has(this.first_name) || has(this.last_name)` to require last_name when first_name is present.
How do the Protovalidate language implementations stay in sync?
Through conformance testing. The README links utilities for acceptance testing of implementations at docs/conformance.md, so the spec and each runtime are checked against shared tests rather than only against their own unit tests.
What does Protovalidate's CI check for schema changes?
The Makefile lint-proto target runs `buf lint` and then `buf breaking --against '.git#branch=main'`, which fails the build on breaking schema changes, plus a separate step that checks invariants of validate.proto itself.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/bufbuild-protovalidate)Star history chart for your README
[](https://hysenlabs.com/projects/bufbuild-protovalidate)Was this useful?