Skip to content

yeqown/protoc-gen-fieldmask

Repository files navigation

protoc-gen-fieldmask

GitHub release (latest SemVer) GitHub go.mod Go version License

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.

Features

  • 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*() and Masked*() 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

Use Cases

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

Installation

Install from source

# 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-fieldmask

Install from remote

go install github.com/yeqown/protoc-gen-fieldmask/cmd/protoc-gen-fieldmask@latest

Quick Start

1. Define Your Proto

syntax = "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}];
}

2. Generate Code

protoc \
  -I. \
  -I$YOUR_PROTO_PATH \
  --go_out=paths=source_relative:. \
  --fieldmask_out=paths=source_relative,lang=go:. \
  example.proto

This generates:

  • example.pb.go - Standard protobuf generated code
  • example.pb.fm.go - FieldMask utility code

3. Use the Generated API

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 name

Server 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
}

API Reference

Method Options

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)
  };
}

Field Options

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}];
}

Generated Methods

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)

Examples

See the examples directory for complete working examples:

  • complete/ - End-to-end demonstration with all scenarios
  • proto/ - Proto definitions used in examples

Run the complete example:

cd examples/complete
go run .

Mask Modes

FILTER Mode (Partial Response)

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 returned

PRUNE Mode (Partial Update)

Use 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 updated

Nested Messages

Generate 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()

Debugging

To debug code generation:

  1. Install protoc-gen-debug:

    go install github.com/lyft/protoc-gen-star/protoc-gen-debug@latest
  2. 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
  3. Run debug test:

    go test -v ./internal/module -run Test_ForDebug

License

MIT

About

Generate FieldMask utility functions for protobuf

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors