Skip to content

Protobuf FieldMask: Let Your API Clients Ask for Only What They Need

July 3, 2026 · 6 min read · Lire en français

Contents

Protobuf FieldMask: Let Your API Clients Ask for Only What They Need

A single endpoint is rarely used by a single client. A mobile app, a web front end, a batch job and another backend service all hit the same GetProduct, and they don't all want the same thing. The mobile app needs a title and a price. The product page wants reviews and recommendations too. The batch job only needs an ID and a timestamp.

The problem: if the server always computes everything, the cheapest caller pays the price of the most expensive one. Some fields are free (they're already in the row you fetched). Others are not: they mean a database aggregation, a call to another service, or a big payload over a slow mobile connection.

FieldMask is a small, standard way for the client to say "return only these fields", and for the server to skip the work behind the fields nobody asked for. This article explains the idea with a classic online-store API, not a copy-paste recipe, so you can apply it to your own services.

A classic scenario

Say you run an online store. Your product service exposes one gRPC method:

service ProductService {
  rpc GetProduct(GetProductRequest) returns (Product);
}

message Product {
  string id             = 1;
  string title          = 2;
  Money  price          = 3;
  ReviewSummary reviews = 4;  // aggregation over the reviews table
  repeated Product recommendations = 5;  // call to the reco service
  StockInfo stock       = 6;  // fan-out to every warehouse
}

Look at the cost of each field:

FieldCost to produce
id, title, pricefree, already in the product row
reviewsa GROUP BY aggregation over millions of rows
recommendationsa remote call to the recommendation service
stocka fan-out to every warehouse, then a sum

A phone showing a search result only needs title and price. But if GetProduct always builds the full Product, that phone triggers a reviews aggregation, a reco call and a stock fan-out it will never display. Multiply by thousands of requests per second and you're burning CPU, saturating downstream services, and shipping bytes no one reads.

The tempting fix that doesn't scale

The first instinct is boolean flags:

message GetProductRequest {
  string id = 1;
  bool include_reviews = 2;
  bool include_recommendations = 3;
  bool include_stock = 4;
}

It works, until it doesn't. Every new expensive field adds another include_* flag. Nested fields (I want reviews.average but not the full breakdown) have no clean expression. And every client has to know your private list of flags. It's a combinatorial mess that grows with your schema.

You want one mechanism that scales to any field, nests naturally, and is part of the standard toolbox. That's FieldMask.

What FieldMask actually is

google.protobuf.FieldMask is a well-known type shipped with Protocol Buffers. It's almost nothing, just a list of field paths:

message FieldMask {
  repeated string paths = 1;
}

A path is a field name, dot-separated for nesting: title, price, reviews.average. That's it. The client fills it in, the server reads it. It's the gRPC cousin of REST's ?fields=title,price and of GraphQL's field selection, a sparse fieldset: ask for a subset, get a subset.

You add one field to your request:

message GetProductRequest {
  string id = 1;
  google.protobuf.FieldMask field_mask = 2;
}

And the client asks for exactly what it needs:

field_mask {
  paths: "title"
  paths: "price"
}

Reading with a mask

Here's the flow. The mask does two distinct jobs, and it's important not to confuse them:

Client  ── field_mask { title, price } ──►  Server
                                             │
                                             │ 1. GUARD: only do the expensive
                                             │    work for requested fields
                                             │       reviews?   not asked → skip
                                             │       recos?     not asked → skip
                                             │       stock?     not asked → skip
                                             │
                                             │ 2. FILTER: trim the response
                                             │    down to the requested paths
                                             ▼
Client  ◄──  Product { title, price }  ──  Server

Job 1: guard the expensive work. This is the one that saves money, and the server must do it by hand. FieldMask does not magically skip your code; it just tells you what's safe to skip. In Go:

func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {
    paths := req.GetFieldMask().GetPaths()
    wants := func(f string) bool { return len(paths) == 0 || slices.Contains(paths, f) }

    p, err := s.loadProductRow(ctx, req.GetId()) // cheap: id, title, price
    if err != nil {
        return nil, err
    }

    if wants("reviews") {
        p.Reviews = s.aggregateReviews(ctx, p.Id) // expensive, only if asked
    }
    if wants("recommendations") {
        p.Recommendations = s.fetchRecommendations(ctx, p.Id) // remote call
    }
    if wants("stock") {
        p.Stock = s.fanOutStock(ctx, p.Id) // fan-out
    }

    return p, nil
}

Note the len(paths) == 0 fallback: an empty mask means "everything". A client that doesn't know or care about FieldMask keeps working exactly as before. That's what makes the feature safe to add to an existing API.

Job 2: filter the response. Even after guarding, a field might be populated when the client didn't ask for it (defaults, cached values, a field that's free to compute). Trimming the message to the mask keeps the contract honest and the payload small. The protobuf runtime ships a helper for this: in Java it's FieldMaskUtil.merge(); most languages have an equivalent. Applied over the built response, it strips every path not in the mask.

Writes use the same idea

FieldMask isn't only for reads. On an update, the same mask answers a different question, "which fields am I allowed to change?", which is exactly what PATCH means in REST:

message UpdateProductRequest {
  Product product = 1;
  google.protobuf.FieldMask update_mask = 2;
}

With update_mask { paths: "price" }, the server touches only the price and leaves the rest alone. But writes come with their own rules: clearing a field versus leaving it untouched, nested paths, and the danger of an empty mask (which on a write means replace everything, the exact opposite of reads). That's a topic of its own, covered in the follow-up: Protobuf FieldMask for writes →.

The traps

  • Field names become part of your API contract. On the wire, protobuf identifies fields by number, so renaming a field is normally safe. With FieldMask the client sends the field name as a string, so rename reviews to review_summary and every existing mask breaks silently. Rule: don't rename masked fields. If you must, keep accepting the old name in the server until every caller has migrated, then deprecate.
  • The mask doesn't skip anything by itself. It's just a list of strings. If you forget the if wants(...) guards, you compute everything and gain nothing. The savings live in your code, not in the framework.
  • Repeated fields can't be sub-selected. You can ask for recommendations, but not recommendations.title, because protobuf FieldMask has no syntax for "this sub-field, inside each element of a list." Know the limit before you design around it.
  • Ship sensible default masks. Don't make every client hand-write paths. Expose ready-made masks in your client library for the common cases (ProductMasks.SUMMARY, ProductMasks.FULL) so callers opt into a curated set instead of guessing field names.

Recap

Without FieldMaskWith FieldMask
One endpoint computes everything for everyoneEach caller declares the fields it needs
Cheap callers pay for expensive fieldsExpensive work runs only when requested
A new include_* flag per expensive fieldOne mechanism, any field, nesting included
PATCH is ambiguous (empty = clear or ignore?)The update mask says exactly what changes

FieldMask is a tiny addition (one field on the request) that turns a rigid, one-size-fits-all endpoint into a flexible one, without a flag explosion and without breaking existing clients (empty mask = everything). The catch is that it only pays off if you wire the guards yourself: the mask tells you what you're allowed to skip, but skipping the work is still your job.

Subscribe to future posts

Get future posts in your inbox. No spam, unsubscribe any time.

Powered by Buttondown.

Related posts

© 2026 < Denis AKPAGNONITE /> | N1BBzerLZXT