Skip to content

Latest commit

 

History

History
177 lines (131 loc) · 5.89 KB

README.md

File metadata and controls

177 lines (131 loc) · 5.89 KB

Validate

Build Report Card GoDoc

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.

Installation

go get connectrpc.com/validate

A small example

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!

FAQ

Does this interceptor work with Connect clients?

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.

How do clients know which fields are invalid?

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.

How should schemas import protovalidate's options?

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.

Does the interceptor validate responses?

No. On both clients and servers, the interceptor only validates requests.

Ecosystem

Status: Unstable

This module is unstable. Expect breaking changes as we iterate toward a stable release.

It supports:

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.

License

Offered under the Apache 2 license.