Generate FieldMask utilities for protobuf. Support Go with more programming languages planned.
FieldMask is a protobuf message type used to represent a set of fields that should be contained in a response, sent to the Client. It's similar to GraphQL but operates on the server side.
This plugin generates type-safe utilities to deal with FieldMask messages, helping developers avoid repetitive code.
- Method-level options - Configure FieldMask behavior per RPC method
- Field-level options - Fine-grained control over individual field masking behavior
- Type-safe API - Generated
Mask*()andMasked*()methods for compile-time safety - Two mask modes:
- FILTER mode - Only marked fields are included (partial response)
- PRUNE mode - Marked fields are excluded (partial update)
- Nested message support - Generate mask methods for nested message types
- Request/Response separation - Separate APIs for request and response field masking
1. Partial Response (FILTER mode)
- Reduce response payload for mobile clients
- Hide sensitive data (passwords, internal fields)
- On-demand field loading based on client needs
2. Partial Update (PRUNE mode)
- Incremental updates - only update specified fields
- Prevent accidental modification of protected fields
# Clone the repository
git clone https://github.com/yeqown/protoc-gen-fieldmask.git
cd protoc-gen-fieldmask
# Install the plugin
make install
# or
go install ./cmd/protoc-gen-fieldmaskgo install github.com/yeqown/protoc-gen-fieldmask/cmd/protoc-gen-fieldmask@latestsyntax = "proto3";
package example.user.v1;
import "google/protobuf/field_mask.proto";
import "protoc_gen_fieldmask/option.proto";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (protoc_gen_fieldmask.rpc) = {
field_name: "fm"
mode: FILTER // Only marked fields are returned
};
}
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse) {
option (protoc_gen_fieldmask.rpc) = {
field_name: "fm"
mode: PRUNE // Marked fields are excluded from update
};
}
}
message GetUserRequest {
string user_id = 1 [(protoc_gen_fieldmask.field) = {mask: true}];
google.protobuf.FieldMask fm = 2;
}
message GetUserResponse {
string user_id = 1 [(protoc_gen_fieldmask.field) = {mask: true}];
string name = 2 [(protoc_gen_fieldmask.field) = {mask: true}];
string email = 3 [(protoc_gen_fieldmask.field) = {mask: true}];
string password_hash = 4; // Sensitive - not marked
}
message UpdateUserRequest {
string user_id = 1;
string name = 2 [(protoc_gen_fieldmask.field) = {mask: true}];
string email = 3 [(protoc_gen_fieldmask.field) = {mask: true}];
google.protobuf.FieldMask fm = 4;
}
message UpdateUserResponse {
string user_id = 1 [(protoc_gen_fieldmask.field) = {mask: true}];
string name = 2 [(protoc_gen_fieldmask.field) = {mask: true}];
string email = 3 [(protoc_gen_fieldmask.field) = {mask: true}];
}protoc \
-I. \
-I$YOUR_PROTO_PATH \
--go_out=paths=source_relative:. \
--fieldmask_out=paths=source_relative,lang=go:. \
example.protoThis generates:
example.pb.go- Standard protobuf generated codeexample.pb.fm.go- FieldMask utility code
Client side (Partial Response):
req := &GetUserRequest{UserId: "user123"}
// Mark which fields you want in the response
fm := req.FieldMask().Response()
fm.MaskUserId()
fm.MaskName()
// email and password_hash are not marked, so they won't be returned
// Call RPC
resp, _ := client.GetUser(ctx, req)
// resp contains only user_id and nameServer side (Partial Response):
func (s *Server) GetUser(ctx context.Context, req *GetUserRequest) (*GetUserResponse, error) {
// Build full response
resp := &GetUserResponse{
UserId: user.Id,
Name: user.Name,
Email: user.Email,
PasswordHash: "secret", // Sensitive data
}
// Apply field mask - only marked fields are kept
req.FieldMask().Response().Apply(resp)
return resp, nil
}Client side (Partial Update):
req := &UpdateUserRequest{
UserId: "user123",
Name: "new_name",
Email: "should_not_change@example.com",
}
// Mark which fields to update
fm := req.FieldMask().Request()
fm.MaskName() // Only name will be updated
// Email is not marked, so it won't be updated
// Call RPC
resp, _ := client.UpdateUser(ctx, req)Server side (Partial Update):
func (s *Server) UpdateUser(ctx context.Context, req *UpdateUserRequest) (*UpdateUserResponse, error) {
user, _ := s.getUserFromStore(req.UserId)
fm := req.FieldMask()
// Update only marked fields
if fm.Request().MaskedName() {
user.Name = req.Name
}
// Email is not checked/updated because it wasn't marked
return &UpdateUserResponse{...}, nil
}Configure FieldMask behavior on RPC methods:
rpc Method(Request) returns (Response) {
option (protoc_gen_fieldmask.rpc) = {
field_name: "fm"; // FieldMask field name in Request (default: "fm")
mode: FILTER; // FILTER or PRUNE (default: FILTER)
};
}Configure individual field behavior:
message Message {
// Generate mask methods for this field
string field_name = 1 [(protoc_gen_fieldmask.field) = {mask: true}];
// Generate mask methods for this nested message type
NestedMessage nested = 2 [(protoc_gen_fieldmask.field) = {mask: true, nested: true}];
}For each message with FieldMask:
// Get the FieldMask controller
func (x *Request) FieldMask() *Method_FieldMask
// Access request/response mask controllers
func (fm *Method_FieldMask) Request() *Method_Req
func (fm *Method_FieldMask) Response() *Method_Res
// Mark fields (Request - for partial update)
func (r *Method_Req) MaskFieldName()
func (r *Method_Req) MaskNested_FieldName()
// Mark fields (Response - for partial response)
func (r *Method_Res) MaskFieldName()
func (r *Method_Res) MaskNested_FieldName()
// Check if fields are marked
func (r *Method_Req) MaskedFieldName() bool
func (r *Method_Res) MaskedFieldName() bool
// Apply mask to response message
func (r *Method_Res) Apply(resp proto.Message)See the examples directory for complete working examples:
Run the complete example:
cd examples/complete
go run .Use when: Client wants to specify which fields to include in response
Behavior:
- Client marks fields they want
- Server returns only marked fields
- Unmarked fields are cleared (set to zero/empty)
Example:
// Mobile client - only needs ID and name
fm.Response().MaskUserId()
fm.Response().MaskName()
// Result: Only user_id and name are returnedUse when: Client wants to specify which fields to update
Behavior:
- Client marks fields they want to update
- Server updates only marked fields
- Unmarked fields are left unchanged
Example:
// Update only name and email
fm.Request().MaskName()
fm.Request().MaskEmail()
// Result: Only name and email are updatedGenerate mask methods for nested message types:
message Address {
string country = 1 [(protoc_gen_fieldmask.field) = {mask: true}];
string province = 2 [(protoc_gen_fieldmask.field) = {mask: true}];
}
message GetUserResponse {
string user_id = 1 [(protoc_gen_fieldmask.field) = {mask: true}];
Address address = 2 [(protoc_gen_fieldmask.field) = {mask: true, nested: true}];
}Usage:
fm.Response().MaskAddress()
fm.Response().MaskAddress_Country()
fm.Response().MaskAddress_Province()To debug code generation:
-
Install
protoc-gen-debug:go install github.com/lyft/protoc-gen-star/protoc-gen-debug@latest
-
Generate debug data:
make prepare-debug # or manually: protoc \ -I=./examples/pb \ -I=./proto \ --plugin=protoc-gen-debug=$(which protoc-gen-debug) \ --debug_out="./internal/module/debugdata:." \ ./examples/pb/user.proto
-
Run debug test:
go test -v ./internal/module -run Test_ForDebug

