connectrpc.com/validate
provides a Connect interceptor that
takes the tedium out of data validation. Rather than hand-writing repetitive
documentation and code — verifying that User.email
is valid, or that
User.age
falls within reasonable bounds — you can instead encode those
constraints into your Protobuf schemas and automatically enforce them at
runtime.
Under the hood, this package is powered by protovalidate and the Common Expression Language. Together, they make validation flexible, efficient, and consistent across languages without additional code generation.
go get connectrpc.com/validate
Curious what all this looks like in practice? First, let's define a schema for our user service:
syntax = "proto3";
package example.user.v1;
import "buf/validate/validate.proto";
import "google/protobuf/timestamp.proto";
message User {
// Simple constraints, like checking that an email address is valid, are
// predefined.
string email = 1 [(buf.validate.field).string.email = true];
// For more complex use cases, like comparing fields against each other, we
// can write a CEL expression.
google.protobuf.Timestamp birth_date = 2;
google.protobuf.Timestamp signup_date = 3;
option (buf.validate.message).cel = {
id: "user.signup_date",
message: "signup date must be on or after birth date",
expression: "this.signup_date >= this.birth_date"
};
}
message CreateUserRequest {
User user = 1;
}
message CreateUserResponse {
User user = 1;
}
service UserService {
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
}
Notice that simple constraints, like checking email addresses, are short and declarative. When we need a more elaborate constraint, we can write a custom CEL expression, customize the error message, and much more. (See the main protovalidate repository for more examples.)
After implementing UserService
, we can add a validating interceptor with just
one option:
package main
import (
"context"
"fmt"
"log"
"net/http"
"connectrpc.com/connect"
"connectrpc.com/validate"
userv1 "connectrpc.com/validate/internal/gen/example/user/v1"
"connectrpc.com/validate/internal/gen/validate/example/v1/userv1connect"
)
func main() {
interceptor, err := validate.NewInterceptor()
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.Handle(userv1connect.NewUserServiceHandler(
&userv1connect.UnimplementedUserServiceHandler{},
connect.WithInterceptors(interceptor),
))
http.ListenAndServe("localhost:8080", mux)
}
With the validate.Interceptor
applied, our UserService
implementation can
assume that all requests have already been validated — no need for
hand-written boilerplate!
Yes: it validates request messages before sending them to the server. But unless you're sure that your clients always have an up-to-date schema, it's better to let the server handle validation.
If the request message fails validation, the interceptor returns an error coded
with connect.CodeInvalidArgument
. It also adds a detailed representation of the
validation error(s) as an error detail.
Because this interceptor uses protovalidate, it doesn't
need any generated code for validation. However, any Protobuf schemas with
constraints must import buf/validate/validate.proto
. It's
easiest to import this file directly from the Buf Schema
Registry: this repository contains an example
schema with constraints,
buf.yaml and buf.gen.yaml
configuration files, and make generate
recipe.
No. On both clients and servers, the interceptor only validates requests.
- connect-go: the Connect runtime
- protovalidate-go: the underlying Protobuf validation library
- protovalidate: schemas and documentation for the constraint language
- CEL: the Common Expression Language
This module is unstable. Expect breaking changes as we iterate toward a stable release.
It supports:
- The three most recent major releases of Go. Keep in mind that only the last two releases receive security patches.
- APIv2 of Protocol Buffers in Go (
google.golang.org/protobuf
).
Within those parameters, this project follows semantic versioning. Once we tag a stable release, we will not make breaking changes without incrementing the major version.
Offered under the Apache 2 license.