Skip to content

The semantic validation library for Protobuf

Protobuf gives you type safety. Protovalidate gives you data correctness. Define validation rules directly on your schemas and enforce them identically in Go, JavaScript/TypeScript, Java, Python, and C++.

This Protobuf schema is perfectly valid:

message User {
string id = 1;
string email = 2;
string name = 3;
int32 age = 4;
}

And so is this data:

{"id": "", "email": "not-an-email", "name": "", "age": -5}

Every field has the right type. Every field has the wrong value. Without semantic validation, this data flows through your system unchecked — corrupting databases, breaking dashboards, propagating downstream.

Add validation rules directly to your schema:

message User {
// Must be a valid UUID
string id = 1 [(buf.validate.field).string.uuid = true];
// Must be a valid email address
string email = 2 [(buf.validate.field).string.email = true];
// Must not be empty, max 100 characters
string name = 3 [
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 100
];
// Must be between 0 and 150
int32 age = 4 [
(buf.validate.field).int32.gte = 0,
(buf.validate.field).int32.lte = 150
];
}

Now the garbage data from above is rejected at the boundary. The rules are self-documenting, machine-enforceable, and travel with the schema everywhere it goes.

Standard rules cover most of what you need

Section titled “Standard rules cover most of what you need”

Protovalidate ships with built-in rules for the validations you write over and over again:

// String formats
string email = 1 [(buf.validate.field).string.email = true];
string uri = 2 [(buf.validate.field).string.uri = true];
string country_code = 3 [(buf.validate.field).string.pattern = "^[A-Z]{2}$"];
// Numeric bounds
int32 quantity = 4 [
(buf.validate.field).int32.gt = 0,
(buf.validate.field).int32.lte = 10000
];
// Required fields
string tenant_id = 5 [(buf.validate.field).required = true];
// Repeated field constraints
repeated string tags = 6 [
(buf.validate.field).repeated.min_items = 1,
(buf.validate.field).repeated.max_items = 10
];
// Enum validation — reject unspecified values
MyEnum status = 7 [(buf.validate.field).enum.defined_only = true];
// Duration ranges
google.protobuf.Duration timeout = 8 [
(buf.validate.field).duration.lte = {seconds: 3600}
];

String formats, numeric bounds, required fields, repeated constraints, enums, durations, timestamps, maps, and more — browse the full reference.

For cross-field validation and complex business logic, Protovalidate supports CEL — a fast, safe expression language that evaluates identically across all supported languages.

Ensure a time range is valid:

message ScheduleEventRequest {
google.protobuf.Timestamp start_time = 1 [(buf.validate.field).required = true];
google.protobuf.Timestamp end_time = 2 [(buf.validate.field).required = true];
// end_time must come after start_time
option (buf.validate.message).cel = {
id: "end_after_start"
message: "end_time must be after start_time"
expression: "this.end_time > this.start_time"
};
}

Catch accidental data entry errors:

message AddContactRequest {
string first_name = 1 [
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 50
];
string last_name = 2 [
(buf.validate.field).string.min_len = 1,
(buf.validate.field).string.max_len = 50
];
string email_address = 3 [
(buf.validate.field).string.email = true
];
// Prevent name fields from matching the email address
option (buf.validate.message).cel = {
id: "name.not.email"
message: "name fields cannot match email address"
expression: "this.first_name != this.email_address && this.last_name != this.email_address"
};
}

Standard rules plus CEL means there’s no validation you can’t express. See the custom rules guide for more.

if err := protovalidate.Validate(message); err != nil {
// Handle failure.
}

Go quickstart

Define once, enforce everywhere. No per-language reimplementation. No drift between services.

Play with rules, break things, see validation errors in real time — no setup required.