Extensions
Extensions are fields that are defined outside the message they extend. They allow third-party code to attach data to a message type without modifying its original definition.
Defining extensions
An extension is declared in a .proto file with the extend keyword:
syntax = "proto2";
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
string custom_option = 50000;
}
The code generator produces an Extension object for each extension:
Using extensions
Extensions are accessed through the same container protocol as regular fields, with the Extension object as the key:
from gen.options_pb import ext_custom_option
from protobuf.wkt import FieldOptions
opts = FieldOptions()
# Set
opts[ext_custom_option] = "hello"
# Presence check
ext_custom_option in opts # True
# Get
opts[ext_custom_option] # "hello"
# Clear
del opts[ext_custom_option]
ext_custom_option in opts # False
opts[ext_custom_option] # "" (zero value)
Extensions always track presence; ext in msg returns True even when the extension is set to its zero value:
Extensions in custom options
The most common use of extensions is to define custom options: annotations that attach metadata to fields, messages, or services in a schema.
This is how tools like protovalidate, gRPC gateway, and OpenAPI generators communicate configuration through the .proto file itself.
Defining a custom option
Create a file proto/options.proto that extends one of the Protobuf descriptor option messages:
syntax = "proto2";
package example;
import "google/protobuf/descriptor.proto";
// Mark a field as containing sensitive data that should be redacted in logs.
extend google.protobuf.FieldOptions {
optional bool sensitive = 50001;
}
Using the option
Annotate a field in your message with the option:
syntax = "proto3";
package example;
import "options.proto";
message User {
string first_name = 1;
string last_name = 2 [(example.sensitive) = true];
bool active = 3;
}
Regenerate code with buf generate.
The generated options_pb.py exports the ext_sensitive extension object.
Reading the option at runtime
Access the field's descriptor and use the extension to read the option value:
from gen.options_pb import ext_sensitive
from gen.user_pb import User
for field in User.desc().fields:
if field.proto.options is not None and field.proto.options[ext_sensitive]:
print(f"{field.name} is sensitive")
# last_name is sensitive
This pattern lets you write generic tools (log redactors, schema validators, documentation generators) that work across any message by inspecting the descriptors and options at runtime. See Custom Options for more on reading options through reflection.
Extensions and JSON
Serializing or deserializing a message that contains extensions to/from ProtoJSON requires a Registry with the extension registered.
Extensions not found in the registry are silently omitted from JSON output.
from protobuf import Registry
registry = Registry()
registry.add(ext_custom_option) # the extension to register
text = opts.to_json(registry=registry)
opts = FieldOptions.from_json(text, registry=registry)
How extensions are stored
Extensions are stored in the message's unknown fields in their wire format and parsed on each access. This means importing a file that defines extensions has no side effects: there is no global registration step, and extensions from different files can never conflict.