Skip to content

Java quickstart#

Getting started with Protovalidate is simple if you're familiar with Java and Buf—otherwise, you may want to follow the step-by-step example.

  1. Add buf.build/bufbuild/protovalidate to buf.yaml then buf dep update.
  2. Add validation rules and generate code. Don't forget managed mode configuration.
    message User {
        string name = 1 [(buf.validate.field).required = true];
    }
    
  3. Add build.buf:protovalidate to your Gradle or Maven dependencies.
  4. Validate Protobuf messages:
    Validator validator = ValidatorFactory.newBuilder().build();
    ValidationResult result = validator.validate(message);
    
  5. Validate RPC requests with interceptors. See gRPC and Java for an example.

Step-by-step example#

Start by setting up the example project:

  1. Install the Buf CLI. If you already have, run buf --version to verify that you're using at least 1.54.0.
  2. Have git and Java 17+ installed.
  3. Clone the buf-examples repository:

    $ git clone https://github.com/bufbuild/buf-examples.git
    
  4. Open a terminal to the repository and navigate to protovalidate/quickstart-java/start.

The quickstart code contains Buf CLI configuration files (buf.yaml, buf.gen.yaml), a simple weather_service.proto, and an idiomatic unit test.

Add Protovalidate to schemas#

Depend on Protovalidate#

Protovalidate is available through the Buf Schema Registry and provides the Protobuf extensions, options, and messages powering validation.

Add it as a dependency in buf.yaml:

buf.yaml
version: v2
modules:
  - path: proto
+ deps:
+   - buf.build/bufbuild/protovalidate
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Next, update dependencies. You may see a warning that Protovalidate hasn't yet been used. That's fine.

$ buf dep update
WARN    Module buf.build/bufbuild/protovalidate is declared in your buf.yaml deps but is unused...

Add rules to a message#

Open proto/bufbuild/weather/v1/weather_service.proto, import Protovalidate, and add validation rules to GetWeatherRequest.

proto/bufbuild/weather/v1/weather_service.proto
syntax = "proto3";

package bufbuild.weather.v1;

+ import "buf/validate/validate.proto";
import "google/protobuf/timestamp.proto";

// GetWeatherRequest is a request for weather at a point on Earth.
message GetWeatherRequest {
  // latitude must be between -90 and 90, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
- float latitude = 1;
+ float latitude = 1 [
+   (buf.validate.field).float.gte = -90,
+   (buf.validate.field).float.lte = 90
+ ];

  // longitude must be between -180 and 180, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
- float longitude = 2;
+ float longitude = 2 [
+   (buf.validate.field).float.gte = -180,
+   (buf.validate.field).float.lte = 180
+ ];

  // forecast_date for the weather request. It must be within the next
  // three days.
  google.protobuf.Timestamp forecast_date = 3;
}

Try it in the Playground#

Experiment with Protovalidate rules in the Protovalidate playground—modify this example, try out a custom CEL rule, or write your own validation logic without any local setup.

Lint your changes#

Some rule combinations compile successfully but fail at runtime. This example requires latitude but also skips its validation when it has its zero value, creating a logical contradiction:

Example IGNORE_IF_ZERO_VALUE lint error
message GetWeatherRequest {
  float latitude = 1 [
    (buf.validate.field).ignore = IGNORE_IF_ZERO_VALUE,
    (buf.validate.field).required = true,
    (buf.validate.field).float.gte = -90,
    (buf.validate.field).float.lte = 90
  ];
}

buf lint identifies these and other problems, like invalid CEL expressions, with its PROTOVALIDATE rule :

Buf lint errors for the PROTOVALIDATE rule
$ buf lint
proto/bufbuild/weather/v1/weather_service.proto:29:5:Field "latitude" has both
(buf.validate.field).required and (buf.validate.field).ignore=IGNORE_IF_ZERO_VALUE.
A field cannot be empty if it is required.

Run buf lint whenever you edit your schemas and in GitHub Actions or other CI/CD tools.

Build the module#

Now that you've added Protovalidate as a dependency, updated your schema with rules, and validated changes with buf lint, your module should build with no errors:

$ buf build

Generate code#

With Protovalidate, you don't need any new code generation plugins: its rules are compiled as part of your message descriptors.

Managed mode requirements#

Update your managed mode options in buf.gen.yaml, or your generated code won't compile:

buf.gen.yaml
version: v2
inputs:
  - directory: src/main/proto
plugins:
  - remote: buf.build/protocolbuffers/java:v29.3
  out: src/main/java
managed:
  enabled: true
+ disable:
+   - file_option: java_package
+     module: buf.build/bufbuild/protovalidate

Run buf generate to include your new rules in the GetWeatherRequest descriptor:

$ buf generate

To learn more about generating code with the Buf CLI, read the code generation overview.

Add business logic with CEL#

Real world validation rules are often complicated and need more than a simple set of static rules:

  1. A BuyMovieTicketsRequest request must be for a showtime in the future but no more than two weeks in the future.
  2. A CreateTeamRequest with repeated members must ensure all email addresses are unique across the team.
  3. A ScheduleMeetingRequest must have a start_time before its end_time, and the meeting duration can't exceed 8 hours.

Protovalidate can meet all of these requirements because all Protovalidate rules are defined in Common Expression Language (CEL). CEL is a lightweight, high-performance expression language that allows expressions like this.first_flight_duration + this.second_flight_duration < duration('48h') to evaluate consistently across languages.

Adding a CEL-based rule to a field is straightforward. Instead of a providing a static value, you provide a unique identifier (id), an error message, and a CEL expression. Building on the prior GetWeatherRequest example, add a custom rule stating that users must ask for weather forecasts within the next 72 hours:

proto/bufbuild/weather/v1/weather_service.proto
syntax = "proto3";

package bufbuild.weather.v1;

import "buf/validate/validate.proto";
import "google/protobuf/timestamp.proto";

// GetWeatherRequest is a request for weather at a point on Earth.
message GetWeatherRequest {
  // latitude must be between -90 and 90, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
  float latitude = 1 [
    (buf.validate.field).float.gte = -90,
    (buf.validate.field).float.lte = 90
  ];
  // longitude must be between -180 and 180, inclusive, to be valid. Use of a
  // float allows precision to about one square meter.
  float longitude = 2 [
    (buf.validate.field).float.gte = -180,
    (buf.validate.field).float.lte = 180
  ];

  // forecast_date for the weather request. It must be within the next
  // three days.
- google.protobuf.Timestamp forecast_date = 3;
+ google.protobuf.Timestamp forecast_date = 3 [(buf.validate.field).cel = {
+     id: "forecast_date.within_72_hours"
+     message: "Forecast date must be in the next 72 hours."
+     expression: "this >= now && this <= now + duration('72h')"
+ }];
}

Remember to recompile and regenerate code:

$ buf generate

Run validation#

The example code has a failing test (WeatherTest). Let's get it to pass, using Protovalidate's Java API to validate sample messages.

  1. Open build.gradle.kts and verify that libs.protovalidate has already been added as a dependency. In your own projects, you'd need to add build.buf:protovalidate as a dependency.

    build.gradle
    dependencies {
        implementation(libs.protobuf.java)
        implementation(libs.protovalidate)
    
        testImplementation platform('org.junit:junit-bom:5.10.0')
        testImplementation 'org.junit.jupiter:junit-jupiter'
    }
    
  2. Run WeatherTest with ./gradlew test. It should fail: it expects invalid latitudes and longitudes to be rejected, but you haven't yet added any validation.

    $ ./gradlew test
    > Task :test FAILED
    
    WeatherTest > TestBadLatitude() FAILED
        org.opentest4j.AssertionFailedError at WeatherTest.java:56
    
    WeatherTest > TestValidRequest() PASSED
    
    WeatherTest > TestBadLongitude() FAILED
        org.opentest4j.AssertionFailedError at WeatherTest.java:73
    
    WeatherTest > TestBadForecastDate() FAILED
        org.opentest4j.AssertionFailedError at WeatherTest.java:90
    
  3. Open WeatherService (in src/main/java/bufbuild/weather). Update the validateGetWeatherRequest function to return the result of validator.validate():

    WeatherService
    public class WeatherService {
    
        private static final Validator validator = ValidatorFactory.newBuilder().build();
    
        public ValidationResult validateGetWeatherRequest(GetWeatherRequest request) throws ValidationException {
    -       return new ValidationResult(Collections.emptyList());
    +       return validator.validate(request);
        }
    }
    
  4. Run ./gradlew test. Now that you've added validation, all tests should pass.

    $ ./gradlew test
    

You've now walked through the basic steps for using Protovalidate: adding it as a dependency, annotating your schemas with rules, and validating Protobuf messages.

Validate RPC requests#

One of Protovalidate's most common use cases is for validating requests made to RPCs. See gRPC and Java for an example that uses an interceptor to automatically validate requests.

Next steps#

Read on to learn more about enabling schema-first validation with Protovalidate: