Protobuf FieldMask : laissez vos clients d'API ne demander que ce dont ils ont besoin
3 juillet 2026 · 7 min read · Read in English
Sommaire
Protobuf FieldMask : laissez vos clients d'API ne demander que ce dont ils ont besoin
Un endpoint est rarement utilisé par un seul client. Une app mobile, un front web, un job batch et un autre service backend tapent tous le même GetProduct et ils ne veulent pas la même chose. L'app mobile veut un titre et un prix. La page produit veut aussi les avis et les recommandations. Le batch ne veut qu'un ID et une date.
Le problème : si le serveur calcule tout à chaque fois, l'appelant le moins gourmand paie le prix du plus gourmand. Certains champs sont gratuits (ils sont déjà dans la ligne qu'on a lue). D'autres non : ils veulent dire une agrégation SQL, un appel à un autre service, ou un gros payload sur une connexion mobile lente.
FieldMask est une manière standard et minimale pour le client de dire « ne me renvoie que ces champs », et pour le serveur de sauter le travail derrière les champs que personne n'a demandés. Cet article explique l'idée avec une API de boutique en ligne classique, pas une recette à copier-coller, pour que vous puissiez l'appliquer à vos propres services.
Un scénario classique
Imaginons une boutique en ligne. Votre service produit expose une méthode gRPC :
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
}
message Product {
string id = 1;
string title = 2;
Money price = 3;
ReviewSummary reviews = 4; // agrégation sur la table des avis
repeated Product recommendations = 5; // appel au service de reco
StockInfo stock = 6; // fan-out vers chaque entrepôt
}
Regardez le coût de chaque champ :
| Champ | Coût pour le produire |
|---|---|
id, title, price | gratuit, déjà dans la ligne produit |
reviews | un GROUP BY sur des millions de lignes |
recommendations | un appel distant au service de recommandation |
stock | un fan-out vers chaque entrepôt, puis une somme |
Un téléphone qui affiche un résultat de recherche n'a besoin que de title et price. Mais si GetProduct construit toujours le Product complet, ce téléphone déclenche une agrégation d'avis, un appel de reco et un fan-out de stock qu'il n'affichera jamais. Multipliez par des milliers de requêtes par seconde et vous brûlez du CPU, vous saturez les services en aval, et vous envoyez des octets que personne ne lit.
La solution tentante qui ne passe pas à l'échelle
Le premier réflexe, ce sont les booléens :
message GetProductRequest {
string id = 1;
bool include_reviews = 2;
bool include_recommendations = 3;
bool include_stock = 4;
}
Ça marche, jusqu'à ce que ça ne marche plus. Chaque nouveau champ coûteux ajoute un flag include_*. Les champs imbriqués (je veux reviews.average mais pas le détail complet) n'ont aucune expression propre. Et chaque client doit connaître votre liste privée de flags. C'est un casse-tête combinatoire qui grossit avec votre schéma.
Ce qu'on veut, c'est un seul mécanisme qui passe à l'échelle sur n'importe quel champ, s'imbrique naturellement, et fait partie de la boîte à outils standard. C'est FieldMask.
Ce qu'est réellement FieldMask
google.protobuf.FieldMask est un type bien connu (well-known type) livré avec Protocol Buffers. C'est presque rien : juste une liste de chemins de champs :
message FieldMask {
repeated string paths = 1;
}
Un chemin est un nom de champ, séparé par des points pour l'imbrication : title, price, reviews.average. C'est tout. Le client le remplit, le serveur le lit. C'est le cousin gRPC du ?fields=title,price de REST et de la sélection de champs de GraphQL, un jeu de champs partiel (sparse fieldset) : demandez un sous-ensemble, recevez un sous-ensemble.
Vous ajoutez un champ à votre requête :
message GetProductRequest {
string id = 1;
google.protobuf.FieldMask field_mask = 2;
}
Et le client demande exactement ce dont il a besoin :
field_mask {
paths: "title"
paths: "price"
}
Lire avec un masque
Voici le flux. Le masque fait deux tâches distinctes, et il ne faut pas les confondre :
Client ── field_mask { title, price } ──► Serveur
│
│ 1. GARDE : ne fait le travail
│ coûteux que pour les champs demandés
│ reviews ? pas demandé → skip
│ recos ? pas demandé → skip
│ stock ? pas demandé → skip
│
│ 2. FILTRE : réduit la réponse
│ aux seuls chemins demandés
▼
Client ◄── Product { title, price } ── Serveur
Tâche 1 : garder le travail coûteux. C'est celle qui fait économiser, et le serveur doit la faire à la main. FieldMask ne saute pas votre code par magie ; il vous dit seulement ce qu'il est sûr de sauter. En 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()) // pas cher : id, title, price
if err != nil {
return nil, err
}
if wants("reviews") {
p.Reviews = s.aggregateReviews(ctx, p.Id) // coûteux, seulement si demandé
}
if wants("recommendations") {
p.Recommendations = s.fetchRecommendations(ctx, p.Id) // appel distant
}
if wants("stock") {
p.Stock = s.fanOutStock(ctx, p.Id) // fan-out
}
return p, nil
}
Notez le repli len(paths) == 0 : un masque vide veut dire « tout ». Un client qui ne connaît pas ou se moque de FieldMask continue de fonctionner exactement comme avant. C'est ce qui rend la fonctionnalité sûre à ajouter sur une API existante.
Tâche 2 : filtrer la réponse. Même après la garde, un champ peut être rempli sans que le client l'ait demandé (valeurs par défaut, valeurs en cache, champ gratuit à calculer). Réduire le message au masque garde le contrat honnête et le payload léger. Le runtime protobuf fournit un utilitaire pour ça : en Java c'est FieldMaskUtil.merge(), la plupart des langages ont un équivalent. Appliqué sur la réponse construite, il retire tout chemin absent du masque.
L'écriture, c'est la même idée
FieldMask n'est pas que pour la lecture. Sur une mise à jour, le même masque répond à une autre question, « quels champs ai-je le droit de modifier ? », c'est exactement ce que veut dire PATCH en REST :
message UpdateProductRequest {
Product product = 1;
google.protobuf.FieldMask update_mask = 2;
}
Avec update_mask { paths: "price" }, le serveur ne touche qu'au prix et laisse tout le reste intact. Mais l'écriture a ses propres règles : effacer un champ ou le laisser tel quel, les chemins imbriqués, et le danger d'un masque vide (qui, en écriture, veut dire tout remplacer, exactement l'inverse de la lecture). C'est un sujet à part entière, traité dans la suite : Protobuf FieldMask pour l'écriture →.
Les pièges
- Les noms de champs deviennent une partie de votre contrat d'API. Sur le fil, protobuf identifie les champs par numéro, donc renommer un champ est normalement sans risque. Avec FieldMask, le client envoie le nom du champ sous forme de chaîne, donc renommez
reviewsenreview_summaryet tous les masques existants cassent silencieusement. Règle : ne renommez pas un champ masqué. Si vous devez le faire, continuez d'accepter l'ancien nom côté serveur jusqu'à ce que tous les appelants aient migré, puis dépréciez-le. - Le masque ne saute rien tout seul. Ce n'est qu'une liste de chaînes. Si vous oubliez les gardes
if wants(...), vous calculez tout et vous ne gagnez rien. Les économies vivent dans votre code, pas dans le framework. - Les champs répétés ne se sous-sélectionnent pas. Vous pouvez demander
recommendations, mais pasrecommendations.title, car le FieldMask de protobuf n'a pas de syntaxe pour « ce sous-champ, dans chaque élément d'une liste ». Connaissez la limite avant de concevoir autour. - Livrez des masques par défaut sensés. N'obligez pas chaque client à écrire ses chemins à la main. Exposez des masques prêts à l'emploi dans votre bibliothèque cliente pour les cas courants (
ProductMasks.SUMMARY,ProductMasks.FULL), pour que les appelants choisissent un jeu soigné plutôt que de deviner des noms de champs.
Récapitulatif
| Sans FieldMask | Avec FieldMask |
|---|---|
| Un endpoint calcule tout pour tout le monde | Chaque appelant déclare les champs qu'il veut |
| Les appelants légers paient les champs coûteux | Le travail coûteux ne tourne que sur demande |
Un flag include_* par champ coûteux | Un seul mécanisme, tout champ, imbrication incluse |
| PATCH ambigu (vide = effacer ou ignorer ?) | Le masque de mise à jour dit exactement quoi changer |
FieldMask est un ajout minuscule (un champ sur la requête) qui transforme un endpoint rigide et uniforme en un endpoint flexible, sans explosion de flags et sans casser les clients existants (masque vide = tout). Le hic : ça ne paie que si vous branchez les gardes vous-même. Le masque vous dit ce que vous avez le droit de sauter, mais sauter le travail reste votre boulot.
S'abonner aux prochains articles
Recevez les nouveaux articles par e-mail. Pas de spam, désinscription à tout moment.
Related posts
Protobuf FieldMask pour l'écriture : un seul endpoint d'update, zéro ambiguïté
Une seule méthode Update qui ne modifie que les champs nommés, sans endpoint par champ ni écrasement du reste. Comment l'update_mask apporte une sémantique PATCH propre à gRPC : mettre à jour, effacer, chemins imbriqués, et le piège du masque vide qui peut effacer des données en silence.
July 7, 2026
Déployer une API Express.js avec Docker
Comment déployer une API REST Express.js avec Docker, avec un focus sur les performances MongoDB, les builds Docker multi-stage, Jest pour les tests, et Nginx comme reverse proxy.
April 11, 2024
Maîtriser NestJS : libérer la puissance des relations avec TypeORM et les bases SQL
Libère la puissance des relations de données avec NestJS, TypeORM et les bases SQL. Maîtrise l'art de construire des structures de données complexes et des interactions fluides. Idéal pour les développeurs NestJS expérimentés comme pour les débutants qui veulent créer des applications à la pointe.
October 18, 2023