[{"data":1,"prerenderedAt":1528},["ShallowReactive",2],{"article-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations":3,"article-sibling-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations":814,"surround-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations":1489,"related-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations":1496},{"id":4,"title":5,"body":6,"date":796,"description":797,"draft":798,"extension":799,"img":800,"meta":801,"navigation":234,"path":802,"seo":803,"slug":804,"stem":805,"tags":806,"topics":811,"__hash__":813},"content\u002F2.backend\u002F2.api-design\u002F2.protobuf-fieldmask-mutations.fr.md","Protobuf FieldMask pour l'écriture : un seul endpoint d'update, zéro ambiguïté",{"type":7,"value":8,"toc":788},"minimark",[9,13,28,31,36,39,93,96,117,143,200,204,211,256,263,271,282,286,297,360,367,373,383,386,559,570,574,586,611,615,638,651,654,684,688,769,775,784],[10,11,5],"h1",{"id":12},"protobuf-fieldmask-pour-lécriture-un-seul-endpoint-dupdate-zéro-ambiguïté",[14,15,16,17,22,23,27],"p",{},"Dans la ",[18,19,21],"a",{"href":20},"\u002Ffr\u002Fblog\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask","première partie",", FieldMask permettait au client de dire ",[24,25,26],"em",{},"« ne me renvoie que ces champs »"," pour que le serveur saute le travail coûteux en lecture. Le même petit type rend un service tout aussi utile côté écriture et là, l'enjeu est plus élevé, car une erreur ne gaspille pas du CPU, elle corrompt des données.",[14,29,30],{},"Cette partie parle de mutations : mettre à jour un produit sans un endpoint par champ, et sans effacer par accident les champs qu'on ne voulait pas toucher. Même exemple de boutique en ligne qu'avant, même outil, sens inverse.",[32,33,35],"h2",{"id":34},"les-deux-mauvaises-options","Les deux mauvaises options",[14,37,38],{},"Vous voulez laisser les clients modifier un produit. Prenez ce message :",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-protobuf shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","message Product {\n  string id          = 1;\n  string title       = 2;\n  Money  price       = 3;\n  string description = 4;\n  Promotion promotion = 5;  \u002F\u002F imbriqué : { discount_pct, ends_at }\n}\n","protobuf","",[47,48,49,57,63,69,75,81,87],"code",{"__ignoreMap":45},[50,51,54],"span",{"class":52,"line":53},"line",1,[50,55,56],{},"message Product {\n",[50,58,60],{"class":52,"line":59},2,[50,61,62],{},"  string id          = 1;\n",[50,64,66],{"class":52,"line":65},3,[50,67,68],{},"  string title       = 2;\n",[50,70,72],{"class":52,"line":71},4,[50,73,74],{},"  Money  price       = 3;\n",[50,76,78],{"class":52,"line":77},5,[50,79,80],{},"  string description = 4;\n",[50,82,84],{"class":52,"line":83},6,[50,85,86],{},"  Promotion promotion = 5;  \u002F\u002F imbriqué : { discount_pct, ends_at }\n",[50,88,90],{"class":52,"line":89},7,[50,91,92],{},"}\n",[14,94,95],{},"Sans FieldMask, vous êtes coincé entre deux designs désagréables.",[14,97,98,102,103,106,107,106,110,106,113,116],{},[99,100,101],"strong",{},"Option 1 : un RPC par champ."," ",[47,104,105],{},"UpdateTitle",", ",[47,108,109],{},"UpdatePrice",[47,111,112],{},"UpdateDescription",[47,114,115],{},"UpdatePromotionEndDate","… Chaque nouveau champ, c'est une nouvelle méthode, un nouveau handler, une nouvelle ligne côté client. L'API grossit sans limite et chaque consommateur doit suivre.",[14,118,119,126,127,130,131,134,135,138,139,142],{},[99,120,121,122,125],{},"Option 2 : un ",[47,123,124],{},"Update"," qui prend tout l'objet (PUT)."," Surface plus simple, mais le client doit désormais envoyer le produit ",[24,128,129],{},"complet"," à chaque fois, même pour changer un seul champ. Pire, c'est ambigu : si la ",[47,132,133],{},"description"," reçue est vide, le client voulait-il ",[24,136,137],{},"« effacer la description »"," ou ",[24,140,141],{},"« je ne l'ai simplement pas renseignée »"," ? Avec un PUT en remplacement total, « pas renseigné » et « mis à vide » sont identiques, si bien qu'un client qui lit, change le prix et renvoie l'objet peut effacer en silence tout champ qu'il n'a pas re-rempli.",[144,145,146,161],"table",{},[147,148,149],"thead",{},[150,151,152,155,158],"tr",{},[153,154],"th",{},[153,156,157],{},"Un RPC par champ",[153,159,160],{},"PUT objet complet",[162,163,164,176,187],"tbody",{},[150,165,166,170,173],{},[167,168,169],"td",{},"Surface d'API",[167,171,172],{},"explose avec le schéma",[167,174,175],{},"une méthode",[150,177,178,181,184],{},[167,179,180],{},"Taille du payload",[167,182,183],{},"minuscule",[167,185,186],{},"tout l'objet à chaque fois",[150,188,189,192,195],{},[167,190,191],{},"« Effacer » vs « laisser »",[167,193,194],{},"s\u002Fo",[167,196,197],{},[99,198,199],{},"ambigu : risque de perte de données",[32,201,203],{"id":202},"le-masque-en-fait-un-patch-propre","Le masque en fait un PATCH propre",[14,205,206,207,210],{},"Ajoutez un ",[47,208,209],{},"update_mask"," et l'ambiguïté disparaît :",[40,212,214],{"className":42,"code":213,"language":44,"meta":45,"style":45},"service ProductService {\n  rpc UpdateProduct(UpdateProductRequest) returns (Product);\n}\n\nmessage UpdateProductRequest {\n  Product product = 1;\n  google.protobuf.FieldMask update_mask = 2;\n}\n",[47,215,216,221,226,230,236,241,246,251],{"__ignoreMap":45},[50,217,218],{"class":52,"line":53},[50,219,220],{},"service ProductService {\n",[50,222,223],{"class":52,"line":59},[50,224,225],{},"  rpc UpdateProduct(UpdateProductRequest) returns (Product);\n",[50,227,228],{"class":52,"line":65},[50,229,92],{},[50,231,232],{"class":52,"line":71},[50,233,235],{"emptyLinePlaceholder":234},true,"\n",[50,237,238],{"class":52,"line":77},[50,239,240],{},"message UpdateProductRequest {\n",[50,242,243],{"class":52,"line":83},[50,244,245],{},"  Product product = 1;\n",[50,247,248],{"class":52,"line":89},[50,249,250],{},"  google.protobuf.FieldMask update_mask = 2;\n",[50,252,254],{"class":52,"line":253},8,[50,255,92],{},[14,257,258,259,262],{},"La règle tient en une phrase : ",[99,260,261],{},"le serveur ne modifie que les champs listés dans le masque, et ignore tout le reste du payload."," Pour changer le prix et rien d'autre :",[40,264,269],{"className":265,"code":267,"language":268},[266],"language-text","product     { price { amount: 1999 currency: \"EUR\" } }\nupdate_mask { paths: \"price\" }\n","text",[47,270,267],{"__ignoreMap":45},[14,272,273,106,276,106,278,281],{},[47,274,275],{},"title",[47,277,133],{},[47,279,280],{},"promotion"," sont absents du masque, donc le serveur les laisse exactement tels qu'ils sont en base, même s'ils sont vides dans le payload. Le client n'envoie que ce qu'il touche. C'est PATCH, exprimé en un champ standard.",[32,283,285],{"id":284},"modifier-et-effacer-avec-le-même-mécanisme","Modifier et effacer avec le même mécanisme",[14,287,288,289,292,293,296],{},"Voici la partie élégante. Le masque dit ",[24,290,291],{},"quels"," champs changent ; le payload dit ",[24,294,295],{},"par quoi",". Combinez les deux et vous obtenez « modifier » et « effacer » d'un seul design :",[144,298,299,318],{},[147,300,301],{},[150,302,303,309,315],{},[153,304,305,306,308],{},"Champ dans ",[47,307,209],{}," ?",[153,310,311,312,308],{},"Valeur dans ",[47,313,314],{},"product",[153,316,317],{},"Résultat",[162,319,320,334,347],{},[150,321,322,325,328],{},[167,323,324],{},"oui",[167,326,327],{},"présente",[167,329,330,333],{},[99,331,332],{},"modifié"," vers la nouvelle valeur",[150,335,336,338,341],{},[167,337,324],{},[167,339,340],{},"vide\u002Fabsente",[167,342,343,346],{},[99,344,345],{},"effacé"," : remis à la valeur par défaut\u002Fnulle",[150,348,349,352,355],{},[167,350,351],{},"non",[167,353,354],{},"peu importe",[167,356,357],{},[99,358,359],{},"laissé intact",[14,361,362,363,366],{},"Ainsi, pour augmenter le prix ",[24,364,365],{},"et"," retirer la date de fin de promotion en un seul appel :",[40,368,371],{"className":369,"code":370,"language":268},[266],"product     { price { amount: 1999 currency: \"EUR\" } }\nupdate_mask { paths: \"price\"  paths: \"promotion.ends_at\" }\n",[47,372,370],{"__ignoreMap":45},[14,374,375,378,379,382],{},[47,376,377],{},"price"," est dans le masque avec une valeur → mis à jour. ",[47,380,381],{},"promotion.ends_at"," est dans le masque mais sans valeur dans le payload → effacé. Tout le reste → intact. Une requête, une modification et un effacement, aucune ambiguïté.",[14,384,385],{},"Côté serveur, vous parcourez le masque et appliquez chaque chemin :",[40,387,391],{"className":388,"code":389,"language":390,"meta":45,"style":45},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","func (s *server) UpdateProduct(ctx context.Context, req *pb.UpdateProductRequest) (*pb.Product, error) {\n    mask := req.GetUpdateMask()\n    if mask == nil || len(mask.GetPaths()) == 0 {\n        return nil, status.Error(codes.InvalidArgument, \"update_mask is required\")\n    }\n\n    current, err := s.load(ctx, req.GetProduct().GetId())\n    if err != nil {\n        return nil, err\n    }\n\n    for _, path := range mask.GetPaths() {\n        switch path {\n        case \"id\", \"created_at\":\n            return nil, status.Errorf(codes.InvalidArgument, \"field %q is immutable\", path)\n        case \"title\":\n            current.Title = req.GetProduct().GetTitle()\n        case \"price\":\n            current.Price = req.GetProduct().GetPrice()\n        case \"description\":\n            current.Description = req.GetProduct().GetDescription()\n        case \"promotion.ends_at\":\n            current.Promotion.EndsAt = req.GetProduct().GetPromotion().GetEndsAt() \u002F\u002F nil → effacé\n        default:\n            return nil, status.Errorf(codes.InvalidArgument, \"unknown field %q\", path)\n        }\n    }\n\n    return s.save(ctx, current)\n}\n","go",[47,392,393,398,403,408,413,418,422,427,432,438,443,448,454,460,466,472,478,484,490,496,502,508,514,520,526,532,538,543,548,554],{"__ignoreMap":45},[50,394,395],{"class":52,"line":53},[50,396,397],{},"func (s *server) UpdateProduct(ctx context.Context, req *pb.UpdateProductRequest) (*pb.Product, error) {\n",[50,399,400],{"class":52,"line":59},[50,401,402],{},"    mask := req.GetUpdateMask()\n",[50,404,405],{"class":52,"line":65},[50,406,407],{},"    if mask == nil || len(mask.GetPaths()) == 0 {\n",[50,409,410],{"class":52,"line":71},[50,411,412],{},"        return nil, status.Error(codes.InvalidArgument, \"update_mask is required\")\n",[50,414,415],{"class":52,"line":77},[50,416,417],{},"    }\n",[50,419,420],{"class":52,"line":83},[50,421,235],{"emptyLinePlaceholder":234},[50,423,424],{"class":52,"line":89},[50,425,426],{},"    current, err := s.load(ctx, req.GetProduct().GetId())\n",[50,428,429],{"class":52,"line":253},[50,430,431],{},"    if err != nil {\n",[50,433,435],{"class":52,"line":434},9,[50,436,437],{},"        return nil, err\n",[50,439,441],{"class":52,"line":440},10,[50,442,417],{},[50,444,446],{"class":52,"line":445},11,[50,447,235],{"emptyLinePlaceholder":234},[50,449,451],{"class":52,"line":450},12,[50,452,453],{},"    for _, path := range mask.GetPaths() {\n",[50,455,457],{"class":52,"line":456},13,[50,458,459],{},"        switch path {\n",[50,461,463],{"class":52,"line":462},14,[50,464,465],{},"        case \"id\", \"created_at\":\n",[50,467,469],{"class":52,"line":468},15,[50,470,471],{},"            return nil, status.Errorf(codes.InvalidArgument, \"field %q is immutable\", path)\n",[50,473,475],{"class":52,"line":474},16,[50,476,477],{},"        case \"title\":\n",[50,479,481],{"class":52,"line":480},17,[50,482,483],{},"            current.Title = req.GetProduct().GetTitle()\n",[50,485,487],{"class":52,"line":486},18,[50,488,489],{},"        case \"price\":\n",[50,491,493],{"class":52,"line":492},19,[50,494,495],{},"            current.Price = req.GetProduct().GetPrice()\n",[50,497,499],{"class":52,"line":498},20,[50,500,501],{},"        case \"description\":\n",[50,503,505],{"class":52,"line":504},21,[50,506,507],{},"            current.Description = req.GetProduct().GetDescription()\n",[50,509,511],{"class":52,"line":510},22,[50,512,513],{},"        case \"promotion.ends_at\":\n",[50,515,517],{"class":52,"line":516},23,[50,518,519],{},"            current.Promotion.EndsAt = req.GetProduct().GetPromotion().GetEndsAt() \u002F\u002F nil → effacé\n",[50,521,523],{"class":52,"line":522},24,[50,524,525],{},"        default:\n",[50,527,529],{"class":52,"line":528},25,[50,530,531],{},"            return nil, status.Errorf(codes.InvalidArgument, \"unknown field %q\", path)\n",[50,533,535],{"class":52,"line":534},26,[50,536,537],{},"        }\n",[50,539,541],{"class":52,"line":540},27,[50,542,417],{},[50,544,546],{"class":52,"line":545},28,[50,547,235],{"emptyLinePlaceholder":234},[50,549,551],{"class":52,"line":550},29,[50,552,553],{},"    return s.save(ctx, current)\n",[50,555,557],{"class":52,"line":556},30,[50,558,92],{},[14,560,561,562,565,566,569],{},"Le runtime protobuf fournit aussi un utilitaire de merge (",[47,563,564],{},"FieldMaskUtil.merge()"," en Java, des équivalents ailleurs) qui recopie pour vous les chemins masqués d'un message vers un autre, pratique, mais écrire le ",[47,567,568],{},"switch"," à la main rend explicite la logique modifier\u002Feffacer\u002Frejeter, ce qui, en écriture, vaut bien quelques lignes de plus.",[32,571,573],{"id":572},"les-champs-imbriqués-et-le-mur-des-champs-répétés","Les champs imbriqués, et le mur des champs répétés",[14,575,576,579,580,582,583,585],{},[99,577,578],{},"Les champs imbriqués"," passent par la notation pointée : ",[47,581,381],{}," vise une feuille dans le sous-message sans remplacer tout le ",[47,584,280],{},". Vous descendez aussi profond que votre schéma. C'est précisément pour ça que le masque bat la soupe de flags : l'imbrication est gratuite.",[14,587,588,591,592,595,596,599,600,603,604,106,607,610],{},[99,589,590],{},"Les champs répétés sont le mur."," Un chemin peut nommer un champ répété, mais il ne peut pas aller ",[24,593,594],{},"à l'intérieur",". Mettez ",[47,597,598],{},"recommendations"," dans le masque et vous remplacez toute la liste, car il n'existe pas de ",[47,601,602],{},"recommendations[2].title"," pour patcher un seul élément. Si vous avez besoin d'opérations au niveau des éléments (ajouter, retirer, réordonner), FieldMask ne vous les donnera pas ; modélisez-les comme des méthodes dédiées (",[47,605,606],{},"AddRecommendation",[47,608,609],{},"RemoveRecommendation","). Sachez-le avant de concevoir une « liste éditable » autour d'un masque.",[32,612,614],{"id":613},"le-piège-du-masque-vide-linverse-de-la-lecture","Le piège du masque vide : l'inverse de la lecture",[14,616,617,618,621,622,625,626,629,630,633,634,637],{},"C'est celui qui mord, et c'est l'image miroir du comportement en lecture. En ",[99,619,620],{},"lecture",", un masque vide veut dire par convention ",[24,623,624],{},"« donne-moi tout »",", un défaut sûr et sympathique. En ",[99,627,628],{},"écriture",", cette même convention est dangereuse : un masque vide veut dire ",[24,631,632],{},"« chaque champ du payload est une modification voulue »",", autrement dit ",[99,635,636],{},"remplacement total",". Tout champ que le client n'a pas rempli est effacé.",[14,639,640,641,644,645,647,648,650],{},"Ajoutez maintenant l'évolution du schéma. Vous livrez un nouveau champ ",[47,642,643],{},"warranty",". Un vieux client, écrit avant l'existence de ",[47,646,643],{},", lit un produit, change le prix, et le renvoie sans masque. Son payload n'a pas de ",[47,649,643],{},". La sémantique de remplacement total efface consciencieusement la garantie de chaque produit que ce vieux client met à jour. Perte de données silencieuse et massive, sans que personne n'ait écrit de bug.",[14,652,653],{},"Deux défenses, utilisez les deux :",[655,656,657,667],"ul",{},[658,659,660,663,664,666],"li",{},[99,661,662],{},"Rendez le masque obligatoire en écriture."," Rejetez une mise à jour dont l'",[47,665,209],{}," est vide (comme le fait le handler ci-dessus). Un client doit déclarer son intention explicitement ; « tout changer » ne devrait jamais être le défaut accidentel.",[658,668,669,672,673,106,676,679,680,683],{},[99,670,671],{},"Rejetez les chemins inconnus et immuables."," Échouez sur un chemin non reconnu, et sur les champs qui ne doivent jamais changer par cette méthode (",[47,674,675],{},"id",[47,677,678],{},"created_at",", champs calculés côté serveur \u002F en lecture seule). Mieux vaut un ",[47,681,682],{},"InvalidArgument"," bien visible qu'un écrasement discret.",[32,685,687],{"id":686},"récapitulatif","Récapitulatif",[144,689,690,700],{},[147,691,692],{},[150,693,694,697],{},[153,695,696],{},"Préoccupation",[153,698,699],{},"Comment l'update_mask la traite",[162,701,702,714,722,730,740,748,759],{},[150,703,704,707],{},[167,705,706],{},"Un endpoint par champ",[167,708,709,710,713],{},"Disparu : un seul ",[47,711,712],{},"UpdateProduct",", le masque choisit les champs",[150,715,716,719],{},[167,717,718],{},"Mise à jour partielle (PATCH)",[167,720,721],{},"Seuls les chemins masqués changent ; le reste est intact",[150,723,724,727],{},[167,725,726],{},"Effacer un champ",[167,728,729],{},"Le lister dans le masque, le laisser vide dans le payload",[150,731,732,735],{},[167,733,734],{},"Champs imbriqués",[167,736,737,738],{},"Notation pointée : ",[47,739,381],{},[150,741,742,745],{},[167,743,744],{},"Édition d'éléments de liste",[167,746,747],{},"Non supporté : utilisez des méthodes add\u002Fremove dédiées",[150,749,750,753],{},[167,751,752],{},"Masque vide",[167,754,755,758],{},[99,756,757],{},"Rendez-le obligatoire"," : vide = remplacement total, donc perte de données",[150,760,761,764],{},[167,762,763],{},"Chemins immuables \u002F inconnus",[167,765,766,767],{},"Rejeter avec ",[47,768,682],{},[14,770,771,772,774],{},"FieldMask est le même petit type dans les deux sens. En lecture il sélectionne ce qui revient et vous laisse sauter le travail coûteux ; en écriture il sélectionne ce qui change et vous offre une sémantique PATCH propre, gratuitement. La différence d'enjeu est toute la leçon : une garde oubliée en lecture gaspille un peu de calcul, le même oubli en écriture peut effacer des données. Rendez le masque obligatoire, rejetez ce qui ne doit pas être touché, et l'update à endpoint unique devient à la fois plus simple ",[24,773,365],{}," plus sûr que les alternatives.",[14,776,777,778,783],{},"Retour à la partie 1 : ",[99,779,780],{},[18,781,782],{"href":20},"Protobuf FieldMask pour la lecture →",".",[785,786,787],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":45,"searchDepth":59,"depth":59,"links":789},[790,791,792,793,794,795],{"id":34,"depth":59,"text":35},{"id":202,"depth":59,"text":203},{"id":284,"depth":59,"text":285},{"id":572,"depth":59,"text":573},{"id":613,"depth":59,"text":614},{"id":686,"depth":59,"text":687},"2026-07-07","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.",false,"md",null,{},"\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations.fr",{"title":5,"description":797},"protobuf-fieldmask-mutations","2.backend\u002F2.api-design\u002F2.protobuf-fieldmask-mutations.fr",[807,808,809,810],"gRPC","Protobuf","API-Design","Mutations",[812],"backend","QS78aGrNCBKXM08r_DUYFVINMAjSFZc3szPHIbLx2Ok",{"id":815,"title":816,"body":817,"date":796,"description":1481,"draft":798,"extension":799,"img":800,"meta":1482,"navigation":234,"path":1483,"seo":1484,"slug":804,"stem":1485,"tags":1486,"topics":1487,"__hash__":1488},"content\u002F2.backend\u002F2.api-design\u002F2.protobuf-fieldmask-mutations.md","Protobuf FieldMask for Writes: One Update Endpoint, No Ambiguity",{"type":7,"value":818,"toc":1473},[819,822,834,837,841,844,878,881,895,919,970,974,980,1016,1023,1029,1038,1042,1053,1115,1122,1127,1135,1138,1264,1270,1274,1286,1307,1311,1334,1346,1349,1374,1378,1457,1463,1471],[10,820,816],{"id":821},"protobuf-fieldmask-for-writes-one-update-endpoint-no-ambiguity",[14,823,824,825,829,830,833],{},"In the ",[18,826,828],{"href":827},"\u002Fblog\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask","first part",", FieldMask let a client say ",[24,831,832],{},"\"return only these fields\""," so the server could skip expensive work on reads. The same little type does something just as useful on the write side and there the stakes are higher, because a mistake doesn't waste CPU, it corrupts data.",[14,835,836],{},"This part is about mutations: updating a product without an endpoint per field, and without accidentally erasing the fields you didn't mean to touch. Same online-store example as before, same tool, opposite direction.",[32,838,840],{"id":839},"the-two-bad-options","The two bad options",[14,842,843],{},"You want to let clients change a product. Take this message:",[40,845,847],{"className":42,"code":846,"language":44,"meta":45,"style":45},"message Product {\n  string id          = 1;\n  string title       = 2;\n  Money  price       = 3;\n  string description = 4;\n  Promotion promotion = 5;  \u002F\u002F nested: { discount_pct, ends_at }\n}\n",[47,848,849,853,857,861,865,869,874],{"__ignoreMap":45},[50,850,851],{"class":52,"line":53},[50,852,56],{},[50,854,855],{"class":52,"line":59},[50,856,62],{},[50,858,859],{"class":52,"line":65},[50,860,68],{},[50,862,863],{"class":52,"line":71},[50,864,74],{},[50,866,867],{"class":52,"line":77},[50,868,80],{},[50,870,871],{"class":52,"line":83},[50,872,873],{},"  Promotion promotion = 5;  \u002F\u002F nested: { discount_pct, ends_at }\n",[50,875,876],{"class":52,"line":89},[50,877,92],{},[14,879,880],{},"Without FieldMask you're stuck between two unpleasant designs.",[14,882,883,102,886,106,888,106,890,106,892,894],{},[99,884,885],{},"Option 1: one RPC per field.",[47,887,105],{},[47,889,109],{},[47,891,112],{},[47,893,115],{},"… Every new field is a new method, a new handler, a new line in the client. The API grows without bound and every consumer has to keep up.",[14,896,897,903,904,907,908,910,911,914,915,918],{},[99,898,899,900,902],{},"Option 2: one ",[47,901,124],{}," that takes the whole object (PUT)."," Simpler surface, but now the client must send the ",[24,905,906],{},"complete"," product every time, even to change one field. Worse, it's ambiguous: if the incoming ",[47,909,133],{}," is empty, did the client mean ",[24,912,913],{},"\"clear the description\""," or ",[24,916,917],{},"\"I just didn't set it\"","? With a full-replace PUT, \"didn't set it\" and \"set it to empty\" look identical, so a client that fetches, changes the price, and sends the object back can silently wipe any field it didn't repopulate.",[144,920,921,933],{},[147,922,923],{},[150,924,925,927,930],{},[153,926],{},[153,928,929],{},"One RPC per field",[153,931,932],{},"Full-object PUT",[162,934,935,946,957],{},[150,936,937,940,943],{},[167,938,939],{},"API surface",[167,941,942],{},"explodes with the schema",[167,944,945],{},"one method",[150,947,948,951,954],{},[167,949,950],{},"Payload size",[167,952,953],{},"tiny",[167,955,956],{},"the whole object every time",[150,958,959,962,965],{},[167,960,961],{},"\"Clear\" vs \"leave alone\"",[167,963,964],{},"n\u002Fa",[167,966,967],{},[99,968,969],{},"ambiguous, data loss risk",[32,971,973],{"id":972},"the-mask-makes-it-a-clean-patch","The mask makes it a clean PATCH",[14,975,976,977,979],{},"Add an ",[47,978,209],{}," and the ambiguity disappears:",[40,981,982],{"className":42,"code":213,"language":44,"meta":45,"style":45},[47,983,984,988,992,996,1000,1004,1008,1012],{"__ignoreMap":45},[50,985,986],{"class":52,"line":53},[50,987,220],{},[50,989,990],{"class":52,"line":59},[50,991,225],{},[50,993,994],{"class":52,"line":65},[50,995,92],{},[50,997,998],{"class":52,"line":71},[50,999,235],{"emptyLinePlaceholder":234},[50,1001,1002],{"class":52,"line":77},[50,1003,240],{},[50,1005,1006],{"class":52,"line":83},[50,1007,245],{},[50,1009,1010],{"class":52,"line":89},[50,1011,250],{},[50,1013,1014],{"class":52,"line":253},[50,1015,92],{},[14,1017,1018,1019,1022],{},"The rule is one sentence: ",[99,1020,1021],{},"the server changes only the fields listed in the mask, and ignores everything else in the payload."," To bump the price and nothing else:",[40,1024,1027],{"className":1025,"code":1026,"language":268},[266],"product    { price { amount: 1999 currency: \"EUR\" } }\nupdate_mask { paths: \"price\" }\n",[47,1028,1026],{"__ignoreMap":45},[14,1030,1031,106,1033,106,1035,1037],{},[47,1032,275],{},[47,1034,133],{},[47,1036,280],{}," are absent from the mask, so the server leaves them exactly as they are in storage, even though they're empty in the payload. The client sends only what it touches. That's PATCH, expressed in one standard field.",[32,1039,1041],{"id":1040},"set-and-clear-with-the-same-mechanism","Set and clear with the same mechanism",[14,1043,1044,1045,1048,1049,1052],{},"Here's the elegant part. The mask says ",[24,1046,1047],{},"which"," fields change; the payload says ",[24,1050,1051],{},"what to",". Combine them and you get both \"set\" and \"clear\" from one design:",[144,1054,1055,1073],{},[147,1056,1057],{},[150,1058,1059,1065,1070],{},[153,1060,1061,1062,1064],{},"Field in ",[47,1063,209],{},"?",[153,1066,1067,1068,1064],{},"Value in ",[47,1069,314],{},[153,1071,1072],{},"Result",[162,1074,1075,1089,1102],{},[150,1076,1077,1080,1083],{},[167,1078,1079],{},"yes",[167,1081,1082],{},"present",[167,1084,1085,1088],{},[99,1086,1087],{},"set"," to the new value",[150,1090,1091,1093,1096],{},[167,1092,1079],{},[167,1094,1095],{},"empty\u002Fabsent",[167,1097,1098,1101],{},[99,1099,1100],{},"clear",": reset to default\u002Fnull",[150,1103,1104,1107,1110],{},[167,1105,1106],{},"no",[167,1108,1109],{},"anything",[167,1111,1112],{},[99,1113,1114],{},"left untouched",[14,1116,1117,1118,1121],{},"So to raise the price ",[24,1119,1120],{},"and"," remove the promotion's end date in a single call:",[40,1123,1125],{"className":1124,"code":370,"language":268},[266],[47,1126,370],{"__ignoreMap":45},[14,1128,1129,1131,1132,1134],{},[47,1130,377],{}," is in the mask with a value → updated. ",[47,1133,381],{}," is in the mask but has no value in the payload → cleared. Everything else → untouched. One request, a set and a clear, no ambiguity.",[14,1136,1137],{},"On the server, you walk the mask and apply each path:",[40,1139,1141],{"className":388,"code":1140,"language":390,"meta":45,"style":45},"func (s *server) UpdateProduct(ctx context.Context, req *pb.UpdateProductRequest) (*pb.Product, error) {\n    mask := req.GetUpdateMask()\n    if mask == nil || len(mask.GetPaths()) == 0 {\n        return nil, status.Error(codes.InvalidArgument, \"update_mask is required\")\n    }\n\n    current, err := s.load(ctx, req.GetProduct().GetId())\n    if err != nil {\n        return nil, err\n    }\n\n    for _, path := range mask.GetPaths() {\n        switch path {\n        case \"id\", \"created_at\":\n            return nil, status.Errorf(codes.InvalidArgument, \"field %q is immutable\", path)\n        case \"title\":\n            current.Title = req.GetProduct().GetTitle()\n        case \"price\":\n            current.Price = req.GetProduct().GetPrice()\n        case \"description\":\n            current.Description = req.GetProduct().GetDescription()\n        case \"promotion.ends_at\":\n            current.Promotion.EndsAt = req.GetProduct().GetPromotion().GetEndsAt() \u002F\u002F nil → cleared\n        default:\n            return nil, status.Errorf(codes.InvalidArgument, \"unknown field %q\", path)\n        }\n    }\n\n    return s.save(ctx, current)\n}\n",[47,1142,1143,1147,1151,1155,1159,1163,1167,1171,1175,1179,1183,1187,1191,1195,1199,1203,1207,1211,1215,1219,1223,1227,1231,1236,1240,1244,1248,1252,1256,1260],{"__ignoreMap":45},[50,1144,1145],{"class":52,"line":53},[50,1146,397],{},[50,1148,1149],{"class":52,"line":59},[50,1150,402],{},[50,1152,1153],{"class":52,"line":65},[50,1154,407],{},[50,1156,1157],{"class":52,"line":71},[50,1158,412],{},[50,1160,1161],{"class":52,"line":77},[50,1162,417],{},[50,1164,1165],{"class":52,"line":83},[50,1166,235],{"emptyLinePlaceholder":234},[50,1168,1169],{"class":52,"line":89},[50,1170,426],{},[50,1172,1173],{"class":52,"line":253},[50,1174,431],{},[50,1176,1177],{"class":52,"line":434},[50,1178,437],{},[50,1180,1181],{"class":52,"line":440},[50,1182,417],{},[50,1184,1185],{"class":52,"line":445},[50,1186,235],{"emptyLinePlaceholder":234},[50,1188,1189],{"class":52,"line":450},[50,1190,453],{},[50,1192,1193],{"class":52,"line":456},[50,1194,459],{},[50,1196,1197],{"class":52,"line":462},[50,1198,465],{},[50,1200,1201],{"class":52,"line":468},[50,1202,471],{},[50,1204,1205],{"class":52,"line":474},[50,1206,477],{},[50,1208,1209],{"class":52,"line":480},[50,1210,483],{},[50,1212,1213],{"class":52,"line":486},[50,1214,489],{},[50,1216,1217],{"class":52,"line":492},[50,1218,495],{},[50,1220,1221],{"class":52,"line":498},[50,1222,501],{},[50,1224,1225],{"class":52,"line":504},[50,1226,507],{},[50,1228,1229],{"class":52,"line":510},[50,1230,513],{},[50,1232,1233],{"class":52,"line":516},[50,1234,1235],{},"            current.Promotion.EndsAt = req.GetProduct().GetPromotion().GetEndsAt() \u002F\u002F nil → cleared\n",[50,1237,1238],{"class":52,"line":522},[50,1239,525],{},[50,1241,1242],{"class":52,"line":528},[50,1243,531],{},[50,1245,1246],{"class":52,"line":534},[50,1247,537],{},[50,1249,1250],{"class":52,"line":540},[50,1251,417],{},[50,1253,1254],{"class":52,"line":545},[50,1255,235],{"emptyLinePlaceholder":234},[50,1257,1258],{"class":52,"line":550},[50,1259,553],{},[50,1261,1262],{"class":52,"line":556},[50,1263,92],{},[14,1265,1266,1267,1269],{},"The protobuf runtime also ships a merge helper (",[47,1268,564],{}," in Java, equivalents elsewhere) that copies masked paths from one message onto another for you, handy, but writing the switch by hand makes the set\u002Fclear\u002Freject logic explicit, which on writes is worth the few extra lines.",[32,1271,1273],{"id":1272},"nested-fields-and-the-repeated-field-wall","Nested fields, and the repeated-field wall",[14,1275,1276,1279,1280,1282,1283,1285],{},[99,1277,1278],{},"Nested fields"," work through dot notation: ",[47,1281,381],{}," targets one leaf inside the sub-message without replacing the whole ",[47,1284,280],{},". You can go as deep as your schema. This is exactly why the mask beats a flag soup: nesting is free.",[14,1287,1288,1291,1292,1295,1296,1298,1299,1301,1302,106,1304,1306],{},[99,1289,1290],{},"Repeated fields are the wall."," A path can name a repeated field, but it can't reach ",[24,1293,1294],{},"inside"," it. Put ",[47,1297,598],{}," in the mask and you replace the entire list, because there is no ",[47,1300,602],{}," to patch a single element. If you need element-level operations (add one, remove one, reorder), FieldMask won't give them to you; model them as their own methods (",[47,1303,606],{},[47,1305,609],{},"). Know this before you design an \"editable list\" around a mask.",[32,1308,1310],{"id":1309},"the-empty-mask-trap-the-opposite-of-reads","The empty-mask trap: the opposite of reads",[14,1312,1313,1314,1317,1318,1321,1322,1325,1326,1329,1330,1333],{},"This is the one that bites, and it's the mirror image of the read behavior. On a ",[99,1315,1316],{},"read",", an empty mask conventionally means ",[24,1319,1320],{},"\"give me everything\"",", a safe, friendly default. On a ",[99,1323,1324],{},"write",", that same convention is dangerous: an empty mask means ",[24,1327,1328],{},"\"every field in the payload is an intended change\"",", i.e. ",[99,1331,1332],{},"full replace",". Any field the client didn't populate gets cleared.",[14,1335,1336,1337,1339,1340,1342,1343,1345],{},"Now add schema evolution. You ship a new ",[47,1338,643],{}," field. An old client, written before ",[47,1341,643],{}," existed, fetches a product, changes the price, and sends it back with no mask. Its payload has no ",[47,1344,643],{},". Full-replace semantics dutifully wipe the warranty on every product that old client updates. Silent, widespread data loss, and nobody wrote a bug.",[14,1347,1348],{},"Two defenses, use both:",[655,1350,1351,1360],{},[658,1352,1353,1356,1357,1359],{},[99,1354,1355],{},"Require the mask on writes."," Reject an update with an empty ",[47,1358,209],{}," (as the handler above does). A client must state its intent explicitly; \"change everything\" should never be the accident-default.",[658,1361,1362,1365,1366,106,1368,1370,1371,1373],{},[99,1363,1364],{},"Reject unknown and immutable paths."," Fail on a path you don't recognize, and on fields that must never change through this method (",[47,1367,675],{},[47,1369,678],{},", server-computed\u002Foutput-only fields). Better a loud ",[47,1372,682],{}," than a quiet overwrite.",[32,1375,1377],{"id":1376},"recap","Recap",[144,1379,1380,1390],{},[147,1381,1382],{},[150,1383,1384,1387],{},[153,1385,1386],{},"Concern",[153,1388,1389],{},"How the update_mask handles it",[162,1391,1392,1403,1411,1419,1428,1436,1447],{},[150,1393,1394,1397],{},[167,1395,1396],{},"Endpoint per field",[167,1398,1399,1400,1402],{},"Gone: one ",[47,1401,712],{},", the mask picks the fields",[150,1404,1405,1408],{},[167,1406,1407],{},"Partial update (PATCH)",[167,1409,1410],{},"Only masked paths change; the rest is untouched",[150,1412,1413,1416],{},[167,1414,1415],{},"Clear a field",[167,1417,1418],{},"List it in the mask, leave it empty in the payload",[150,1420,1421,1423],{},[167,1422,1278],{},[167,1424,1425,1426],{},"Dot notation: ",[47,1427,381],{},[150,1429,1430,1433],{},[167,1431,1432],{},"List element edits",[167,1434,1435],{},"Not supported: use dedicated add\u002Fremove methods",[150,1437,1438,1441],{},[167,1439,1440],{},"Empty mask",[167,1442,1443,1446],{},[99,1444,1445],{},"Require it",": empty means full replace, i.e. data loss",[150,1448,1449,1452],{},[167,1450,1451],{},"Immutable \u002F unknown paths",[167,1453,1454,1455],{},"Reject with ",[47,1456,682],{},[14,1458,1459,1460,1462],{},"FieldMask is the same tiny type in both directions. On reads it selects what comes back and lets you skip expensive work; on writes it selects what changes and gives you clean PATCH semantics for free. The difference in stakes is the whole lesson: a forgotten guard on a read wastes a little compute, the same slip on a write can erase data. Make the mask required, reject what shouldn't be touched, and the one-endpoint update becomes both simpler ",[24,1461,1120],{}," safer than the alternatives.",[14,1464,1465,1466,783],{},"Back to part 1: ",[99,1467,1468],{},[18,1469,1470],{"href":827},"Protobuf FieldMask for reads →",[785,1472,787],{},{"title":45,"searchDepth":59,"depth":59,"links":1474},[1475,1476,1477,1478,1479,1480],{"id":839,"depth":59,"text":840},{"id":972,"depth":59,"text":973},{"id":1040,"depth":59,"text":1041},{"id":1272,"depth":59,"text":1273},{"id":1309,"depth":59,"text":1310},{"id":1376,"depth":59,"text":1377},"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.",{},"\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations",{"title":816,"description":1481},"2.backend\u002F2.api-design\u002F2.protobuf-fieldmask-mutations",[807,808,809,810],[812],"V5BrT658T-H1WIXdxrexJ4TsVazFdMqZpK4CIxV-k4c",[1490,1493],{"title":1491,"path":1492},"Protobuf FieldMask : laissez vos clients d'API ne demander que ce dont ils ont besoin","\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask.fr",{"title":1494,"path":1495},"Comment un entretien m'a poussé à créer Obscura, un générateur de mots de passe avec une vraie entropie","\u002Fsecurity\u002Fintroducing-obscura.fr",[1497,1503,1518],{"path":1492,"title":1491,"description":1498,"date":1499,"tags":1500,"topics":1502},"Un seul endpoint, plein d'appelants, et des champs parfois très coûteux à produire. Protobuf FieldMask permet à chaque client de déclarer exactement les champs qu'il veut, pour que le serveur saute les requêtes SQL lourdes, les appels distants et le payload inutiles. L'idée, un exemple classique, et les pièges.","2026-07-03",[807,808,809,1501],"Performance",[812],{"path":1504,"title":1505,"description":1506,"date":1507,"tags":1508,"topics":1515},"\u002Fdocker\u002Fdeploy-expressjs-api-using-docker.fr","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.","2024-04-11",[1509,1510,1511,1512,1513,1514],"Express.JS","API","Node.js","Mongo","CI\u002FCD","Docker",[1516,812,1517],"docker","devops",{"path":1519,"title":1520,"description":1521,"date":1522,"tags":1523,"topics":1526},"\u002Fbackend\u002Fnest-js\u002Funlocking-the-power-of-relationships-with-typeorm.fr","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.","2023-10-18",[1524,1525,1510],"Nest-JS","Mirco-service",[1527,812],"nest-js",1783585187852]