Skip to main content

Rules and Categories

The rules and categories described here belong to the latest v1 release. If you're still using v1beta1 and haven't migrated yet, please refer to the previous reference.

buf provides a carefully curated set of lint rules designed to provide consistency and maintainability across a Protobuf schema of any size and any purpose, but without being so opinionated as to restrict organizations from making the design decisions they need to make for their individual APIs.

buf lint applies individual lint rules across your Protobuf schema, reporting any violations as errors. All lint rules have an ID, and belong to one or more categories. On this page, we'll discuss the available categories, and the individual rules within each category.

Although categories are not required to be in tree form, they can be represented as such. Note this is just a human representation and is not actual configuration.

  • DEFAULT
    • BASIC
      • MINIMAL
  • COMMENTS
  • UNARY_RPC

Style Guide#

The Style Guide provides a concise document that includes all rules in the DEFAULT category, as well as additional recommendations that are not enforced. We provide this for ease of consumption across your various teams, while linking back to this document for rationale for individual rules.

Categories#

buf provides three "main top-level" categories of increasing strictness:

  • MINIMAL
  • BASIC
  • DEFAULT

These provide the majority of lint rules you will want to apply. Additionally, buf provides "extra top-level" categories:

  • COMMENTS
  • UNARY_RPC

These categories enforce additional constraints that users can apply to their Protobuf schema.

MINIMAL#

The MINIMAL category represents what we consider to be fundamental rules for modern Protobuf development. We find these rules so important that these should be required for protoc to produce valid output.

Not applying these rules can lead to a myriad of bad situations across the variety of available Protobuf plugins, especially plugins not built into protoc itself. There is no downside to applying these rules. If you can't tell, we highly recommend abiding by the MINIMAL group for your development sanity.

The MINIMAL category includes several rules. They are listed here and described in more detail below:

  • MINIMAL
    • DIRECTORY_SAME_PACKAGE
    • PACKAGE_DEFINED
    • PACKAGE_DIRECTORY_MATCH
    • PACKAGE_SAME_DIRECTORY

DIRECTORY_SAME_PACKAGE#

This rule checks that all files in a given directory are in the same package.

PACKAGE_DEFINED#

This rule checks that all files have a package declaration.

PACKAGE_SAME_DIRECTORY#

This rule checks that all files with a given package are in the same directory.

PACKAGE_DIRECTORY_MATCH#

This rule checks that all files are in a directory that matches their package name.

Why?#

In short, the MINIMAL category verifies that all files with package foo.bar.baz.v1 are in the directory foo/bar/baz/v1 (relative to the buf.yaml file), and that only one such directory exists. For example, consider the following tree:

.├── buf.yaml└── foo    └── bar        ├── bat        │   └── v1        │       └── bat.proto // package foo.bar.bat.v1        └── baz            └── v1                ├── baz.proto         // package foo.bar.baz.v1                └── baz_service.proto // package foo.bar.baz.v1

protoc doesn't enforce file structure in any way, but you will have a very bad time with many Protobuf plugins across various languages if you do not do this.

This structure has the effect of allowing imports to self-document their package. For example, you will know that the import foo/bar/bat/v1/bat.proto has types in the package foo.bar.bat.v1.

There is no downside to maintaining this structure, and in fact many languages explicitly or effectively enforce such a file structure anyways (for example, Golang and Java).

BASIC#

The BASIC category includes everything from the MINIMAL category, and adds basic style rules that are widely accepted as standard Protobuf style. These rules should generally be applied for all Protobuf schemas.

These style checks represent the "old" Google Style Guide that has been around for years, before elements from the Uber Style Guide were merged in during the spring of 2019.

The following configuration:

buf.yaml
version: v1lint:  use:    - BASIC

Is equivalent to:

buf.yaml
version: v1lint:  use:    - MINIMAL    - ENUM_PASCAL_CASE    - ENUM_VALUE_UPPER_SNAKE_CASE    - FIELD_LOWER_SNAKE_CASE    - MESSAGE_PASCAL_CASE    - ONEOF_LOWER_SNAKE_CASE    - PACKAGE_LOWER_SNAKE_CASE    - RPC_PASCAL_CASE    - SERVICE_PASCAL_CASE    - PACKAGE_SAME_CSHARP_NAMESPACE    - PACKAGE_SAME_GO_PACKAGE    - PACKAGE_SAME_JAVA_MULTIPLE_FILES    - PACKAGE_SAME_JAVA_PACKAGE    - PACKAGE_SAME_PHP_NAMESPACE    - PACKAGE_SAME_RUBY_PACKAGE    - PACKAGE_SAME_SWIFT_PREFIX    - ENUM_FIRST_VALUE_ZERO    - ENUM_NO_ALLOW_ALIAS    - IMPORT_NO_WEAK    - IMPORT_NO_PUBLIC

ENUM_PASCAL_CASE#

This rule checks that enums are PascalCase.

ENUM_VALUE_UPPER_SNAKE_CASE#

This rule checks that enum values are UPPER_SNAKE_CASE.

FIELD_LOWER_SNAKE_CASE#

This rule checks that field names are lower_snake_case.

MESSAGE_PASCAL_CASE#

This rule checks that messages are PascalCase.

ONEOF_LOWER_SNAKE_CASE#

This rule checks that oneof names are lower_snake_case.

PACKAGE_LOWER_SNAKE_CASE#

This rule checks that packages are lower_snake.case.

RPC_PASCAL_CASE#

This rule checks that RPCs are PascalCase.

SERVICE_PASCAL_CASE#

This rule checks that services are PascalCase.

PACKAGE_SAME_*#

buf does not lint file option values, as explained in the What we left out section below. However, it's important that your file option values are consistent across all files in a given Protobuf package if you do use them.

  • PACKAGE_SAME_CSHARP_NAMESPACE checks that all files with a given package have the same value for the csharp_namespace option.
  • PACKAGE_SAME_GO_PACKAGE checks that all files with a given package have the same value for the go_package option.
  • PACKAGE_SAME_JAVA_MULTIPLE_FILES checks that all files with a given package have the same value for the java_multiple_files option.
  • PACKAGE_SAME_JAVA_PACKAGE checks that all files with a given package have the same value for the java_package option.
  • PACKAGE_SAME_PHP_NAMESPACE checks that all files with a given package have the same value for the php_namespace option.
  • PACKAGE_SAME_RUBY_PACKAGE checks that all files with a given package have the same value for the ruby_package option.
  • PACKAGE_SAME_SWIFT_PREFIX checks that all files with a given package have the same value for the swift_prefix` option.

Each of these rules verify if a given file option is used in one file in a given package, it is used in every file in that package.

For example, if we have file foo_one.proto:

// foo_one.protosyntax = "proto3";
package foo.v1;
option go_package = "foov1";option java_multiple_files = true;option java_package = "com.foo.v1";

Another file foo_two.proto with package foo.v1 must have these three options set to the same value, and the other options unset:

// foo_two.protosyntax = "proto3";
package foo.v1;
option go_package = "foov1";option java_multiple_files = true;option java_package = "com.foo.v1";

ENUM_FIRST_VALUE_ZERO#

This rule enforces that the first enum value is the zero value.

This is a proto3 requirement on build, but is not required in proto2 on build. This rule enforces that this is also followed in proto2.

As an example:

syntax = "proto2";
enum Scheme {  // *** DO NOT DO THIS ***  SCHEME_FTP = 1;  SCHEME_UNSPECIFIED = 0;}

The above will result in generated code in certain languages defaulting to SCHEME_FTP instead of SCHEME_UNSPECIFIED.

ENUM_NO_ALLOW_ALIAS#

This rule outlaws the following:

enum Foo {  option allow_alias = true;  FOO_UNSPECIFIED = 0;  FOO_ONE = 1;  FOO_TWO = 1; // no!}

The allow_alias option allows multiple enum values to have the same number. This can lead to issues when working with the JSON representation of Protobuf, a first-class citizen of proto3. If you get a serialized Protobuf value over the wire in binary format, it is unknown what specific enum value it applies to, and JSON usually serialized enum values by name. While in practice, this can lead to hard-to-track bugs if you declare an alias and expect names to be interchangeable.

Instead of having an alias, we recommend deprecating your current enum, and making a new one with the enum value name you want. Or just stick with the current name for your enum value.

IMPORT_NO_WEAK#

This rule outlaws declaring imports as weak. If you didn't know this was possible, forget what you just learned in this sentence, and regardless do not use these.

IMPORT_NO_PUBLIC#

Similar to the IMPORT_NO_WEAK rule, this rule outlaws declaring imports as public. If you didn't know this was possible, forget what you just learned in this sentence, and regardless do not use these.

DEFAULT#

The DEFAULT category includes everything from the BASIC category, as well as some other default style rules.

The following configuration:

buf.yaml
version: v1lint:  use:    - DEFAULT

Is equivalent to:

buf.yaml
version: v1lint:  use:    - BASIC    - ENUM_VALUE_PREFIX    - ENUM_ZERO_VALUE_SUFFIX    - FILE_LOWER_SNAKE_CASE    - RPC_REQUEST_RESPONSE_UNIQUE    - RPC_REQUEST_STANDARD_NAME    - RPC_RESPONSE_STANDARD_NAME    - PACKAGE_VERSION_SUFFIX    - SERVICE_SUFFIX

As per it's name, DEFAULT is also the default set of lint rules used by buf if no configuration is present, and represents our recommendations for modern Protobuf development without being overly burdensome.

ENUM_VALUE_PREFIX#

This rule requires that all enum value names are prefixed with the enum name. For example:

enum Foo {  FOO_UNSPECIFIED = 0;  FOO_ONE = 1;}
message Bar {  enum Baz {    BAZ_UNSPECIFIED = 0;    BAZ_ONE = 1;  }}

Protobuf enums use C++ scoping rules, which makes it not possible to have two enums in the same package with the same enum value name (an exception is when enums are nested, in which case this rule applies within the given message). While you may think that a given enum value name will always be unique across a package, APIs can develop over years, and there are countless examples of developers having to compromise on their enum names due to backwards compatibility issues. For example, you might have the following enum:

enum Scheme {  // Right off the bat, you can't use "UNSPECIFIED" in multiple enums  // in the same package, so you always would have to prefix this anyways.  SCHEME_UNSPECIFIED = 0;  HTTP = 1;  HTTPS = 2;  ...}

Two years later, you have an enum in the same package you want to add, but can't:

// This is a made up example, bear with us.enum SecureProtocol {  SECURE_PROTOCOL_UNSPECIFIED = 0;  // If this enum is in the same package as Scheme, this will  // produce a protoc compile-time error!  HTTPS = 1;  ...}

ENUM_ZERO_VALUE_SUFFIX#

This rule requires that all enum values have a zero value of ENUM_NAME_UNSPECIFIED. For example:

enum Foo {  FOO_UNSPECIFIED = 0;}

The suffix is configurable.

All enums should have a zero value. proto3 does not differentiate between set and unset fields, so if an enum field is not explicitly set, it defaults to the zero value. If an explicit zero value is not part of the enum definition, this will default to the actual zero value of the enum. For example, if you had:

enum Scheme {  // *** DO NOT DO THIS ***  SCHEME_FTP = 0}
message Uri {  Scheme scheme = 1;}

Then any Uri with scheme not explicitly set will default to SCHEME_FTP.

FILE_LOWER_SNAKE_CASE#

This rule says that all .proto files must be named in lower_snake_case.proto. This is the widely accepted standard.

RPC_REQUEST_STANDARD_NAME, RPC_RESPONSE_STANDARD_NAME, RPC_REQUEST_RESPONSE_UNIQUE#

These rules enforce the message name of RPC request/responses, and that all request/responses are unique.

One of the single most important rules to enforce in modern Protobuf development is to have a unique request and response message for every RPC. Separate RPCs should not have their request and response parameters controlled by the same Protobuf message, and if you share a Protobuf message between multiple RPCs, this results in multiple RPCs being affected when fields on this Protobuf message change. Even in simple cases, best practice is to always have a wrapper message for your RPC request and response types. buf enforces this with these three rules by verifying the following:

  • All request and response messages are unique across your Protobuf schema.
  • All request and response messages are named after the RPC, either by naming them MethodNameRequest, MethodNameResponse or ServiceNameMethodNameRequest, ServiceNameMethodNameResponse.

For example, the following service definition abides by these rules:

// request/response message definitions omitted for brevity
service FooService {  rpc Bar(BarRequest) returns (BarResponse) {}  rpc Baz(FooServiceBazRequest) returns (FooServiceBazResponse) {}}

There are configuration options associated with these three rules.

PACKAGE_VERSION_SUFFIX#

This rule enforces that the last component of a package must be a version of the form v\d+, v\d+test.*, v\d+(alpha|beta)\d*, or v\d+p\d+(alpha|beta)\d*, where numbers are >=1.

Examples (all taken directly from buf testing):

foo.v1foo.v2foo.bar.v1foo.bar.v1alphafoo.bar.v1alpha1foo.bar.v1alpha2foo.bar.v1betafoo.bar.v1beta1foo.bar.v1beta2foo.bar.v1p1alphafoo.bar.v1p1alpha1foo.bar.v1p1alpha2foo.bar.v1p1betafoo.bar.v1p1beta1foo.bar.v1p1beta2foo.bar.v1testfoo.bar.v1testfoo

One of the core promises of Protobuf API development is to never have breaking changes in your APIs, and buf helps enforce this through the breaking change detector. However, there are scenarios where you do want to properly version your API. Instead of making changes, the proper method to do so is to make a completely new Protobuf package that is a copy of your existing Protobuf package, serve both packages server-side, and manually migrate your callers. This rule enforces that all packages have a version attached so that it is clear when a package represents a new version.

A common idiom is to use alpha and beta packages for packages that are still in development and can have breaking changes. You can configure the breaking change detector to ignore breaking changes in files for these packages with the ignore_unstable_packages option:

buf.yaml
version: v1breaking:  ignore_unstable_packages: true

SERVICE_SUFFIX#

This rule enforces that all services are suffixed with Service. For example:

service FooService {}service BarService {}service BazService {}

Service names inherently end up having a lot of overlap with package names, and service naming often ends up inconsistent as a result across a larger Protobuf schema. Enforcing a consistent suffix takes away some of this inconsistency.

The suffix is configurable. For example, if you have the following configuration in your buf.yaml:

buf.yaml
version: v1lint:  service_suffix: Endpoint

The SERVICE_SUFFIX rule will enforce the following naming instead:

service FooEndpoint {}service BarEndpoint {}service BazEndpoint {}

COMMENTS#

This is an "extra top-level" category that enforces that comments are present on various parts of your Protobuf schema.

The COMMENTS category includes the following rules:

  • COMMENT_ENUM checks that enums have non-empty comments.
  • COMMENT_ENUM_VALUE checks that enum values have non-empty comments.
  • COMMENT_FIELD checks that fields have non-empty comments.
  • COMMENT_MESSAGE checks that messages have non-empty comments.
  • COMMENT_ONEOF checks that oneof have non-empty comments.
  • COMMENT_RPC checks that RPCs have non-empty comments.
  • COMMENT_SERVICE checks that services have non-empty comments.

Note that only leading comments are considered - trailing comments do not count towards passing these rules.

You may want to at least enforce that certain parts of your schema contain comments. For example, you can select individual rules in the COMMENTS category like so:

buf.yaml
version: v1lint:  use:    - DEFAULT    - COMMENT_ENUM    - COMMENT_MESSAGE    - COMMENT_RPC    - COMMENT_SERVICE

UNARY_RPC#

This is an "extra top-level" category that outlaws streaming RPCs.

This UNARY_RPC category includes the following rules:

  • RPC_NO_CLIENT_STREAMING checks that RPCs are not client streaming.
  • RPC_NO_SERVER_STREAMING checks that RPCs are not server streaming.

Some RPC protocols do not allow streaming RPCs, for example Twirp. This extra category enforces that no developer accidentally adds a streaming RPC if your setup does not support them. Additionally, streaming RPCs have a number of issues in general usage. See this discussion for more details.

What we left out#

We think that the above lint rules represent a set that will sufficiently enforce consistent and maintainable Protobuf schemas, including for large organizations, without being so opinionated as to not let your organization make it's own design decisions. Regardless, there are some potential rules we purposefully did not write that deserve special mention.

File option values#

buf does not include linting for specific file option values. It's not that we don't think consistency across these file options is important - in fact, we think it's very important for easy Protobuf stub consumption. A core principle we feel strongly about is that language-specific file options shouldn't be part of your core Protobuf schema - your Protobuf schema should only describe language-independent elements as much as is possible.

The values for most file options, in fact, should be deduced in a stable and deterministic manner. For example, we think that java_package should likely be a constant prefix followed by the package name as a suffix. Your go_package should use the last component of your package name. And java_multiple_files should always be true. These aren't defaults, for backwards-compatibility reasons, but if you're using a tool like buf to produce your stubs, you shouldn't have to think about any of this.

This is exactly why we've created Managed Mode, which sets all of these file options on the fly with buf generate.

buf still enforces that specific file options are the same across a given package, done through the BASIC and DEFAULT categories described above. We do find this to be important, regardless of what values you choose. Fortunately, with Managed Mode, you can remove your file option declarations altogether and leave the rest to buf.

Custom options#

There are no lint rules for widely-used custom options such as google.api options or protoc-gen-validate. There's a lot of thought that needs to go into issues such as forwards and backwards compatibility for custom options, so we currently only support the standard set of file options. Please contact us if this is a big need for your organization.

Naming opinions#

buf stays away from enforcing naming opinions, such as package name restrictions (beyond versioning requirements and lower_snake_case), or field naming such as google.protobuf.Duration name standardization. This is to provide maximum usefulness of the DEFAULT category out of the box.

Adding or requesting new rules#

If you'd like a new rule added, please contact us to discuss it. We'll add rules if we think they're maintainable and could have widespread value. Most rules can be very easily added, and although buf is OSS, it's usually easier for us to add it ourselves.