A FieldMask lets you specify exactly which fields in a protobuf message should be updated or retrieved, preventing accidental overwrites and reducing network traffic.
Let’s see it in action with a User resource. Imagine we have a UserService with UpdateUser and GetUser methods.
// user.proto
syntax = "proto3";
package user;
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
bool is_active = 5;
}
message UpdateUserRequest {
User user = 1;
google.protobuf.FieldMask update_mask = 2;
}
message GetUserRequest {
string id = 1;
google.protobuf.FieldMask read_mask = 2;
}
service UserService {
rpc UpdateUser(UpdateUserRequest) returns (User);
rpc GetUser(GetUserRequest) returns (User);
}
Here’s how UpdateUser might look in Go. We’re receiving a request with a User object and a FieldMask. The server iterates through the paths in the FieldMask and applies only those fields from the incoming User to the existing user record.
// server.go (simplified for illustration)
func (s *userService) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.User, error) {
existingUser, err := s.userStore.GetUser(ctx, req.User.Id)
if err != nil {
return nil, status.Errorf(codes.NotFound, "user not found: %v", err)
}
for _, path := range req.UpdateMask.Paths {
switch path {
case "name":
existingUser.Name = req.User.Name
case "email":
existingUser.Email = req.User.Email
case "age":
existingUser.Age = req.User.Age
case "is_active":
existingUser.IsActive = req.User.IsActive
// Note: 'id' is typically not updatable via FieldMask
}
}
err = s.userStore.UpdateUser(ctx, existingUser)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to update user: %v", err)
}
return existingUser, nil
}
And GetUser would use the read_mask similarly, but instead of modifying, it would construct a new User message containing only the requested fields.
The problem FieldMask solves is the "lost update" anti-pattern and inefficient data transfer. Without it, a typical UpdateUser might receive a full User object. If two clients try to update the same user concurrently, the second client’s request might overwrite changes made by the first, even if the second client only intended to change a single field. FieldMask ensures that only specified fields are touched. For GetUser, it means you can fetch just the name and email of a user without pulling down their age or is_active status, saving bandwidth and processing time.
The FieldMask itself is a message with a repeated field of strings called paths. Each string is a dot-separated path to a field within the protobuf message. For nested messages, you chain the field names: user.address.street. For repeated fields, you can specify an index like repeated_field[0], but this is less common for updates and more for selection. The special path * can be used to represent all fields, but it defeats the purpose of selective updates and should be used with caution.
When updating, the server’s responsibility is to correctly interpret these paths and apply the corresponding values from the provided message. The google.golang.org/protobuf/types/known/fieldmaskpb package in Go provides utilities to help with this, like Unmarshal and Validate.
The most surprising thing about FieldMask is how it interacts with default values. If a field is set to its default value (e.g., 0 for an integer, "" for a string, false for a boolean) and it’s not present in the FieldMask, it will not be updated. However, if a field is present in the FieldMask, and its value in the incoming message is the default value, the server will update the existing field to that default value. This means you can explicitly clear a field by including it in the FieldMask with its default value.
For example, if a user’s age is 30 and you send an UpdateUserRequest with update_mask containing "age" and the user object having age: 0, the user’s age will be set to 0. If age was not in the update_mask, the user’s age would remain 30.
The next thing you’ll likely encounter is handling FieldMask validation, especially ensuring paths are valid for the target message type.