We recommend completing the tour for an introduction to the buf lint command.

Using a linter on your Protobuf sources enables you to enforce consistency and keep your API definitions in line with your chosen best practices. We recommend enforcing lint rules whether you're working on a small personal project or maintaining a large set of Protobuf definitions across a major organization, but it's especially important for users and organizations that continually onboard new engineers who aren't yet experienced with Protobuf schema design.

The buf CLI provides linting functionality through the buf lint command. When you run buf lint, buf runs a set of lint rules across all the Protobuf files covered by a buf.yaml configuration file. By default, the buf CLI uses a curated set of lint rules designed to guarantee consistency and maintainability across Protobuf schemas of any size and purpose—but without being so opinionated that it restricts you from making the design decisions you need to make for your individual APIs.

Run buf lint --error-format=config-ignore-yaml to get a minimal set of rules to ignore. You can enable lint today and come back to fix any issues another day.

Key Concepts

Some features of the buf CLI's linter:

  • Selectable configuration of the exact lint rules you want, including categorization of lint rules into categories. While we recommend using the DEFAULT set of lint rules, you're free to go your own way.

  • Editor integration. The default error output is easily parsed by most editors, which allows for a tight feedback loop for lint errors. Currently, we provide Vim and Visual Studio Code integration but we may support other editors in the future, such as Emacs and IntelliJ IDEs.

  • Speed. buf's internal Protobuf compiler uses all available cores to compile your Protobuf schemas while maintaining deterministic output. Additionally, it copies files into memory before processing. As an unscientific example, buf can compile all 2,311 .proto files in googleapis in about 0.8 seconds on a four-core machine, while it takes protoc 4.3 seconds to do so on the same machine. While both are fast, the buf CLI provides near-instantaneous feedback, which is especially useful for editor integration. buf's speed is directly proportional to the input size, so linting a single file only takes a few milliseconds.

Configuration

You can configure the buf CLI's linter with a buf.yaml file at the root of the Protobuf source files you want to lint. If you run buf lint against an input that contains a buf.yaml file, the lint configuration in that file is used. If the input doesn't contain a buf.yaml file, the buf CLI operates as if a buf.yaml file with the default values were present.

This example config shows all the available configuration options:

buf.yaml
version: v1
lint:
    use:
        - DEFAULT
    except:
        - FILE_LOWER_SNAKE_CASE
    ignore:
        - bat
        - ban/ban.proto
    ignore_only:
        ENUM_PASCAL_CASE:
            - foo/foo.proto
            - bar
        BASIC:
            - foo
    enum_zero_value_suffix: _UNSPECIFIED
    rpc_allow_same_request_response: false
    rpc_allow_google_protobuf_empty_requests: false
    rpc_allow_google_protobuf_empty_responses: false
    service_suffix: Service
    allow_comment_ignores: true

For more info, see the buf.yaml reference.

Options

use

The use key is optional and lists the IDs or categories to use for linting. For example, this config applies the BASIC lint category and the FILE_LOWER_SNAKE_CASE rule:

buf.yaml
version: v1
lint:
    use:
        - BASIC
        - FILE_LOWER_SNAKE_CASE

The default use value is DEFAULT as a single item:

use:
    - DEFAULT

except

The except key is optional and removes IDs or categories from the use list. For example, this config results in all lint rules in the DEFAULT lint category being used except ENUM_NO_ALLOW_ALIAS and all lint rules in the BASIC category:

buf.yaml
version: v1
lint:
    use:
        - DEFAULT
    except:
        - ENUM_NO_ALLOW_ALIAS
        - BASIC

Note that since DEFAULT is the default value for use, this is equivalent to the above:

buf.yaml
version: v1
lint:
    except:
        - ENUM_NO_ALLOW_ALIAS
        - BASIC

ignore

The ignore key is optional and enables you to exclude directories or files from all lint rules when running buf lint. If a directory is ignored, then all files and subfolders of the directory will also be ignored. The specified directory or file paths must be relative to the buf.yaml. For example, the lint result in foo/bar.proto is ignored with this config:

buf.yaml
version: v1
lint:
    ignore:
        - foo/bar.proto

ignore_only

The ignore_only key is optional and enables you to exclude directories or files from specific lint rules when running buf lint by taking a map from lint rule ID or category to path. As with ignore, the paths must be relative to the buf.yaml

For example, this config sets up specific ignores for the ID ENUM_PASCAL_CASE and the category BASIC:

buf.yaml
version: v1
lint:
    ignore_only:
        ENUM_PASCAL_CASE:
            - foo/foo.proto
            - bar
        BASIC:
            - foo

allow_comment_ignores

The allow_comment_ignores key is optional and turns on comment-driven ignores.

buf.yaml
version: v1
lint:
    allow_comment_ignores: true

If this option is set, leading comments can be added within Protobuf files to ignore lint errors for certain components. If any line in a leading comment starts with buf:lint:ignore ID, then buf ignores lint errors for this ID. For example:

syntax = "proto3";

// buf:lint:ignore PACKAGE_LOWER_SNAKE_CASE
// buf:lint:ignore PACKAGE_VERSION_SUFFIX
package A;

We do not recommend using this. Buf's goal is to help everyone develop consistent Protobuf schemas regardless of organization, and for a large organization it would not be helpful for individual engineers to decide what should and should not be ignored. This should instead be surfaced in a repository-wide configuration file such as buf.yaml.

If you do have specific items that you want to ignore, we recommended adding the offending types to a special file, for example foo_lint_ignore.proto, and setting the corresponding ignore or ignore_only. For example, imagine a legacy enum that uses allow_alias:

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

You could place this enum in a file called foo_lint_ignore.proto and apply this lint configuration:

buf.yaml
version: v1
lint:
    ignore_only:
        ENUM_NO_ALLOW_ALIAS:
            - path/to/foo_lint_ignore.proto

We do recognize, however, that there are situations where comment-driven ignores are necessary, and we want users to be able to make informed decisions, so we added the allow_comment_ignores option. This also has the effect of making it possible to keep comment-driven ignores disabled. If you have commit checks for files via an authors/owners file, for example, you can make sure that buf.yaml is owned by a top-level repository owner and prevent allow_comment_ignores from being set, so that buf ignores any buf:lint:ignore annotations.

enum_zero_value_suffix

The enum_zero_value_suffix key is optional, and controls the behavior of the ENUM_ZERO_VALUE_SUFFIX lint rule. By default, this rule verifies that the zero value of all enums ends in _UNSPECIFIED, as recommended by the Google Protobuf Style Guide. But organizations may have a different preferred suffix, for example _NONE, and enum_zero_value_suffix enables you to set a suffix like this:

buf.yaml
version: v1
lint:
    enum_zero_value_suffix: _NONE

That config allows this:

enum Foo {
  FOO_NONE = 0;
}

rpc_allow_.*

The rpc_allow_same_request_response, rpc_allow_google_protobuf_empty_requests, and rpc_allow_google_protobuf_empty_responses options are optional, and control the behavior of the RPC_REQUEST_STANDARD_NAME, RPC_RESPONSE_STANDARD_NAME, and RPC_REQUEST_RESPONSE_UNIQUE lint rules.

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 straightforward cases, best practice is to always have a wrapper message for your RPC request and response types. buf enforces this as part of the DEFAULT category by verifying that:

  • All requests and responses are unique across your Protobuf schema.
  • All requests and response messages are named after the RPC, either by naming them according to one of these:
    • MethodNameRequest/MethodNameResponse
    • ServiceNameMethodNameRequest/ServiceNameMethodNameResponse

This service definition, for example, abides by these rules:

// request/response message definitions omitted for brevity

service FooService {
  rpc Bar(BarRequest) returns (BarResponse) {}
  rpc Baz(FooServiceBazRequest) returns (FooServiceBazResponse) {}
}

But while do not recommend it, buf provides a few options to loosen these restrictions somewhat:

  • rpc_allow_same_request_response allows the same message type to be used for a single RPC's request and response type.
  • rpc_allow_google_protobuf_empty_requests allows RPC requests to be google.protobuf.Empty messages. This can be set if you want to allow messages to be void forever, that is, to never take any parameters.
  • rpc_allow_google_protobuf_empty_responses allows RPC responses to be google.protobuf.Empty messages. This can be set if you want to allow messages to never return any parameters.

The file google/protobuf/empty.proto is part of the Well-Known Types, and can be directly included in any Protobuf schema. For example:

syntax = "proto3";

package foo.v1;

import "google/protobuf/empty.proto";

service BarService {
  // NOT RECOMMENDED
  rpc Baz(google.protobuf.Empty) returns (google.protobuf.Empty);
}

service_suffix

The service_suffix key is optional, and controls the behavior of the SERVICE_SUFFIX lint rule. By default, this rule verifies that all service names are suffixed with Service. But organizations may have a different preferred suffix, for example API, and service_suffix enables you to set that suffix explicitly:

buf.yaml
version: v1
lint:
    service_suffix: API

That config allows this:

service FooAPI {}

Default values

If a buf.yaml does not exist, or if the lint key is not configured, this default configuration is used:

buf.yaml
version: v1
lint:
    use:
        - DEFAULT
    enum_zero_value_suffix: _UNSPECIFIED
    rpc_allow_same_request_response: false
    rpc_allow_google_protobuf_empty_requests: false
    rpc_allow_google_protobuf_empty_responses: false
    service_suffix: Service

Conclusion

In conclusion, linting is an essential step in ensuring the quality and consistency of your Protobuf files. With buf lint, you have access to an easy-to-use tool that can help you catch and enforce consistency early on in the development process. By following the guidelines and best practices outlined in this overview, you can make the most out of buf lint and produce high-quality Protobuf files that are easier to maintain and scale. Remember to incorporate linting into your development workflow, and continuously refine your approach to improve the overall quality of your codebase. You can achieve this in CI with the following guides: