Aller au contenu

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 :

ChampCoût pour le produire
id, title, pricegratuit, déjà dans la ligne produit
reviewsun GROUP BY sur des millions de lignes
recommendationsun appel distant au service de recommandation
stockun 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 reviews en review_summary et 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 pas recommendations.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 FieldMaskAvec FieldMask
Un endpoint calcule tout pour tout le mondeChaque appelant déclare les champs qu'il veut
Les appelants légers paient les champs coûteuxLe travail coûteux ne tourne que sur demande
Un flag include_* par champ coûteuxUn 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.

Propulsé par Buttondown.

Related posts

© 2026 < Denis AKPAGNONITE /> | N1BBzerLZXT