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:
| Field | Cost to produce |
|---|---|
id, title, price | free, already in the product row |
reviews | a GROUP BY aggregation over millions of rows |
recommendations | a remote call to the recommendation service |
stock | a 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
reviewstoreview_summaryand 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 notrecommendations.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 FieldMask | With FieldMask |
|---|---|
| One endpoint computes everything for everyone | Each caller declares the fields it needs |
| Cheap callers pay for expensive fields | Expensive work runs only when requested |
A new include_* flag per expensive field | One 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.
Related posts
Protobuf FieldMask for Writes: One Update Endpoint, No Ambiguity
A single Update method that changes only the fields you name, with no endpoint per field and no clobbering the rest. How the update_mask brings clean PATCH semantics to gRPC: setting, clearing, nested paths, and the empty-mask trap that can silently wipe data.
July 7, 2026
Deploy Express.js API using Docker
How to deploy Express.js RESTFull API using Docker, with a focus on MongoDB performance, docker multi-stage builds, Jest for testing, and Nginx as a reverse proxy.
April 11, 2024
Mastering NestJS: Unlocking the Power of Relationships with TypeORM and SQL Databases
Unlock the Power of Data Relationships with NestJS, TypeORM, and SQL Databases. Master the art of building complex data structures and seamless interactions. Ideal for both seasoned NestJS developers and beginners looking to create cutting-edge apps
October 18, 2023