L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Design et création · SKILL.md

protobuf-format

protobuf-format

Designs protocol buffer proto3 schemas for data serialization covering well-known types, field behavior annotations, schema evolution strategies, and Buf CLI tooling for type contracts.

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: protobuf-format description: Designs protocol buffer proto3 schemas for data serialization covering well-known types, field behavior annotations, schema evolution strategies, and Buf CLI tooling for type contracts. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: protocol buffers, proto3 schema, protobuf serialization, buf cli, well-known types, wrapper types, schema evolution, how do i design proto schemas archetypes: - tactical - generation anti_triggers: - brainstorming - vague ideation - code golf - over-engineering response_profile: verbosity: medium directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code content-types: [code, guidance, do-dont, examples] related-skills: grpc-patterns, data-encoding, event-schema-versioning


Protocol Buffers Schema Design (proto3)

Designs protocol buffer proto3 schemas for data serialization as a standalone type contract format. Focuses on message schema engineering — field types, well-known types, wrapper types, field behavior annotations, map fields, schema evolution, and Buf CLI tooling. This skill covers the .proto file as a data contract, not gRPC transport.

TL;DR Checklist

  • Use optional keyword for sparse fields in proto3 (not google.protobuf.*Value wrapper types)
  • Select well-known types correctly: Timestamp for time, Duration for spans, Struct for dynamic JSON-like data
  • Apply google.api.field_behavior annotations to REQUIRED and IMMUTABLE fields for code generation contracts
  • Reserve all removed field numbers — never reuse a deleted field's number
  • Add .deprecated = true to every field you intend to remove in the next schema version
  • Use map<key_type, value_type> when you need dictionary semantics; remember maps cannot be repeated
  • Configure Buf lint and breaking detection rules in buf.yaml before publishing a module

When to Use

Use this skill when:

  • Designing or reviewing a .proto file for data serialization between services
  • Choosing between well-known types (Timestamp, Duration, Any, Struct, Value) and custom messages
  • Deciding whether to use optional string vs google.protobuf.StringValue in modern proto3 (2025+)
  • Planning schema evolution strategies: adding, deprecating, or removing fields safely
  • Selecting appropriate field types for IDs (int64 vs string), monetary amounts, and enumerations
  • Setting up Buf CLI tooling (buf.yaml, lint rules, breaking detection) as the modern protoc alternative
  • Defining map fields with specific key/value type constraints

When NOT to Use

Avoid this skill for:

  • Designing gRPC service definitions or transport-layer contracts (use grpc-patterns instead)
  • Implementing gRPC client/server code in Go, Python, or other languages
  • Deciding between REST, JSON, XML, or Avro as a wire format — this skill assumes protobuf is chosen
  • Learning basic Protobuf syntax basics if you already have 2+ years of proto3 experience and need quick lookups

Core Workflow

  1. Define the package namespace — Choose a unique reverse-DNS package name that reflects ownership, not product name. Checkpoint: The package must be namespaced to avoid collisions with third-party WKT imports and other teams' schemas.

  2. Select message structure — Enumerate the top-level messages and their relationships (nested, oneof, map). Checkpoint: Every field in every message must have a concrete type — no bytes for data that should be string, and no repeated primitives without a named wrapper message.

  3. Choose field types wisely — For each field, select the most specific proto3 scalar or well-known type. Use optional for nullable fields in proto3; do not use google.protobuf.*Value wrappers unless you must interoperate with legacy code that expects JSON "value": null. Checkpoint: After typing every field, review: are any int64 IDs better as string? Are monetary values stored as integers in smallest units?

  4. Add semantic annotations — Apply google.api.field_behavior and google.api.resource annotations to enforce contracts at the code generation layer. Checkpoint: Every REQUIRED field must have FIELD_BEHAVIOR_REQUIRED; every immutable identifier must have FIELD_BEHAVIOR_IMMUTABLE.

  5. Plan for schema evolution — Document which fields might change, reserve numbers before removing any field, and use .deprecated = true as a two-phase removal strategy. Checkpoint: No removed field number may be reused within the same package scope. Check the reserved list is complete.

  6. Configure Buf tooling — Create buf.yaml with lint rules, breaking detection rules, and plugin targets. Validate before merging changes. Checkpoint: Run buf lint and buf breaking locally before every commit to .proto files.


Implementation Patterns / Reference Guide

Pattern 1: Well-Known Types — When to Use Each WKT

Proto3 includes 14 well-known types that map to native language types in generated code. Choosing the right WKT matters for both serialization size and developer ergonomics.

// protos/shared/v1/common.proto
// Demonstrates correct selection of well-known types with JSON serialization differences.

syntax = "proto3";

package shared.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/any.proto";

// ✅ GOOD: Timestamp for point-in-time — serializes to ISO 8601 JSON string "2025-01-15T10:30:00Z"
message Order {
  string order_id = 1;
  google.protobuf.Timestamp created_at = 2;        // {"created_at": "2025-01-15T10:30:00Z"}
  google.protobuf.Timestamp updated_at = 3;

  // ✅ GOOD: Duration for time spans — serializes to JSON string "3600.000000001s"
  google.protobuf.Duration cancellation_window = 4;  // {"cancellation_window": "3600s"}

  // ❌ BAD: Using int64 seconds since epoch loses timezone info and requires client-side parsing
  // int64 created_at_epoch = 4;  // Don't do this — use Timestamp instead

  // ✅ GOOD: FieldMask for partial update specification — serializes as JSON array of field paths
  google.protobuf.FieldMask update_mask = 5;        // {"update_mask": ["email", "display_name"]}

  // ✅ GOOD: Struct for arbitrary key-value data when schema is unknown at design time
  map<string, google.protobuf.Value> metadata = 6;  // Dynamic JSON-like payload

  // ⚠️ CAUTION: Any for wrapping heterogeneous messages — loses type safety in serialization
  // Use only when you genuinely need to embed one proto message inside another and know the
  // concrete type at runtime via type_url. For same-package messages, prefer direct nesting.
  google.protobuf.Any attachment = 7;
}

Well-Known Type Selection Guide:

ScenarioChooseJSON SerializationWhy
UTC date-timegoogle.protobuf.Timestamp"2025-01-15T10:30:00Z"ISO 8601 with timezone; native datetime in generated code
Time spangoogle.protobuf.Duration"3600.5s"Human-readable with nanosecond precision
Dynamic key-valuegoogle.protobuf.Struct + Value{"key": "value", "num": 42}Full JSON type mapping; use sparingly — loses schema enforcement
Partial update fieldsgoogle.protobuf.FieldMask["field_a", "nested.field_b"]Standardized for PATCH operations
Wrap unknown messagegoogle.protobuf.Any{"@type": "...", "field": "val"}Type URL + serialized payload; expensive and loses type safety
Empty placeholdergoogle.protobuf.Empty{} (empty object)Method return type when no data needed

Pattern 2: Wrapper Types vs optional — Modern 2025+ Guidance

The old pattern of using google.protobuf.StringValue, google.protobuf.Int32Value, etc. for nullable fields is deprecated for new APIs. Proto3 introduced the optional keyword (since protoc 3.12+) which generates cleaner code and simpler JSON.

// protos/shared/v1/user.proto
// Demonstrates modern optional vs deprecated wrapper types.

syntax = "proto3";

package shared.v1;

import "google/api/field_info.proto";

message UserProfile {
  string id = 1 [(google.api.field_info).format = "UUID"];

  // ✅ GOOD (2025+): Use `optional` keyword — cleaner code, simpler JSON, native nullable types
  optional string display_name = 2;
  optional string email = 3;
  optional int64 age = 4;
  optional bool newsletter_opt_in = 5;

  // ❌ BAD (legacy pattern): google.protobuf.StringValue wrappers — deprecated for new APIs
  // These generate boxed types in most language clients and serialize to JSON as:
  //   {"email": "alice@example.com"}  when present
  //   {}                              when absent (no key emitted)
  // vs optional which emits:
  //   {"email": null}                 when absent (key present, value is null)
  //
  // google.protobuf.StringValue display_name = 6;
  // google.protobuf.StringValue email = 7;

  // ✅ GOOD: Wrapper types are acceptable ONLY when interoperating with legacy JSON APIs
  // that expect `null` to be omitted entirely (not emitted as a key). In that specific case,
  // the wrapper type's "no-key-on-absent" behavior matches the external contract.
}

Key differences — optional vs wrapper types:

Propertyoptional string name = 2;google.protobuf.StringValue name = 2;
Generated code typename: Optional[str] (Python) / *string (Go) / String? (TypeScript)StringValue message instance / pointer to wrapped value
JSON when set{"name": "Alice"}{"name": "Alice"}
JSON when unset{"name": null} or omitted (language-dependent)Key omitted entirely
Wire format when unsetNo bytes emitted (same as absent field)Empty message serialized (tiny overhead)
Code complexityNative nullable typeMessage wrapping/unwrapping required
Recommended for new APIs?Yes — since protoc 3.12+No — deprecated pattern

Pattern 3: Field Behavior Annotations + Deprecation Patterns

Field behavior annotations from google.api.field_behavior.proto provide semantic meaning that code generators can enforce. Combined with deprecation, they form the schema evolution contract layer.

// protos/accounts/v1/account.proto
// Demonstrates field behavior annotations and schema evolution through deprecation.

syntax = "proto3";

package accounts.v1;

import "google/api/field_behavior.proto";
import "google/api/resource.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";

// ✅ GOOD: Resource annotation provides a canonical resource type name
// Used by Google Cloud APIs and gnostic tooling for resource reference resolution.
message Account {
  option (google.api.resource) = {
    type: "accounts.example.com/Account"
    pattern: "accounts/{account}"
    singular: "account"
    plural: "accounts"
  };

  // ✅ GOOD: REQUIRED annotation signals this field must be set before persistence.
  // Code generators and validation libraries can enforce this at the serialization layer.
  string account_id = 1 [
    (google.api.field_behavior) = REQUIRED,
    (google.api.field_behavior) = IMMUTABLE
  ];

  // ✅ GOOD: INPUT_ONLY — never returned in responses. Use for passwords or tokens.
  string password_hash = 2 [(google.api.field_behavior) = INPUT_ONLY];

  // ✅ GOOD: OUTPUT_ONLY — set by the server, ignored on write.
  google.protobuf.Timestamp created_at = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
  google.protobuf.Timestamp deleted_at = 4 [(google.api.field_behavior) = OUTPUT_ONLY];

  // ✅ GOOD: IMMUTABLE after creation — code generators can reject updates to this field.
  string account_type = 5 [
    (google.api.field_behavior) = IMMUTABLE,
    (google.api.field_behavior) = REQUIRED
  ];

  // ✅ GOOD: UNORDERED_LIST indicates set semantics — server should treat as a set, not an ordered sequence.
  repeated string permissions = 6 [(google.api.field_behavior) = UNORDERED_LIST];

  // ⚠️ DEPRECATED: First phase of removal. Keep for 2+ major versions after marking deprecated.
  // Clients receive the field value but should stop sending it. Servers ignore it on input.
  string legacy_email = 7 [
    (google.api.field_behavior) = INPUT_ONLY,
    deprecated = true  // ✅ CRITICAL: Always add .deprecated with any removed field
  ];

  // Reserved numbers from previously deleted fields — NEVER reuse these.
  reserved 8, 9, 10;
  reserved "temp_field_a", "temp_field_b";  // Also reserve former field names for clarity
}

// ✅ GOOD: UpdateAccount request uses FieldMask for partial updates with explicit constraints.
message UpdateAccountRequest {
  Account account = 1 [(google.api.field_behavior) = REQUIRED];
  google.protobuf.FieldMask update_mask = 2 [
    (google.api.field_behavior) = REQUIRED,
    // The mask must only reference OUTPUT_ONLY fields that are allowed to change
  ];
}

Wire format impact of annotations:

  • field_behavior annotations have zero wire format impact — they exist only in the .proto source and are consumed by code generation tools.
  • deprecated = true also has zero wire format impact — the field still serializes/deserializes normally.
  • The actual enforcement of REQUIRED/IMMUTABLE semantics happens in generated code validators, not at the protobuf serialization layer.

Pattern 4: Map Fields and Type Selection Guide

Map fields provide dictionary semantics in proto3 with specific type constraints and limitations you must know before using them.

// protos/shared/v1/types.proto
// Demonstrates map fields, id selection patterns, monetary handling, and oneof alternatives.

syntax = "proto3";

package shared.v1;

import "google/protobuf/timestamp.proto";
import "google/protobuf/struct.proto";

message Transaction {
  // ✅ GOOD: String IDs for external-facing identifiers — stable across system migrations
  // Use string when the ID comes from an external source (UUID, SKU, email) that you don't control.
  string transaction_id = 1;

  // ✅ GOOD: int64 for internal sequential/monotonic IDs — compact wire format (varint-encoded)
  // Use int64 for internally generated, system-owned identifiers where size matters.
  int64 sequence_number = 2;

  // ❌ BAD: Storing monetary values as double — floating point imprecision loses cents
  // double amount = 3;  // 0.1 + 0.2 == 0.30000000000000004 in binary floating point

  // ✅ GOOD: Monetary amounts in smallest currency unit (cents, satoshis, etc.) as int64
  // Client code divides by 10^precision to display; server stores exact integer.
  int64 amount_micros = 4;       // Amount in millionths of a unit (e.g., $12.345678)
  string currency_code = 5;      // ISO 4217 code: "USD", "EUR", "BTC"

  // ✅ GOOD: Map fields for fixed-key lookup semantics
  // Key type must be integral or string (no floats, enums, or messages).
  // Maps are unordered on the wire — iteration order is not guaranteed.
  map<string, int64> balances_by_currency = 6;

  // ⚠️ Map limitation: proto3 does NOT support `repeated map<key, value>`
  // ❌ repeated map<string, int64> multi_balances = 7;  // SYNTAX ERROR — not allowed

  // ✅ ALTERNATIVE: For multiple sets of key-value data, use a repeated message wrapper.
  repeated CurrencyBalance balances = 7;

  // ✅ GOOD: oneof for mutually exclusive field groups — only one can be set at wire level
  // Wire format is efficient: only the selected field's number and value are serialized.
  oneof transfer_type {
    // Only one of these can be present in any single message instance
    AccountRef source_account = 8;
    ExternalPayment external_ref = 9;
  }

  // ✅ GOOD: Nested repeated message as a clean alternative to maps for complex values
  repeated string tags = 10;
}

// Wrapper for multi-currency balances (repeated-map alternative)
message CurrencyBalance {
  string currency_code = 1;
  int64 balance_micros = 2;
}

// oneof variant reference types — avoids circular dependencies with message nesting.
message AccountRef {
  string account_id = 1;
}

message ExternalPayment {
  // Use Struct when the external payment system has arbitrary metadata you can't model statically
  map<string, google.protobuf.Value> extra_fields = 1;
  string provider_ref = 2;
}

Type Selection Quick Reference:

DataBest TypeReason
UUIDs, SKUs, email addressesstringExternal sources are strings; varint-encoding doesn't help
Internal sequential IDsint64Compact varint encoding; monotonically increasing
Monetary amountsint64 (smallest unit)Exact precision; no floating point issues
BooleansboolSingle bit on wire
Counts, quantitiesint32 unless exceeding ±2BSmaller wire size than int64
Large counters, timestamps in secondsint64Exceeds int32 range; 64-bit varint is still efficient for small values
Enumerated categoriesenumSelf-documenting, compact (0-based integer on wire)

Pattern 5: Nested Messages and Depth Guidelines

Excessive nesting increases schema complexity and generated code verbosity. Apply depth limits and reuse patterns.

// protos/shared/v1/address.proto
// Demonstrates nesting depth control and cross-message reuse.

syntax = "proto3";

package shared.v1;

message Address {
  string street_line_1 = 1;
  string street_line_2 = 2;       // Optional second line
  string city = 3;
  string administrative_area = 4; // State/province code
  string postal_code = 5;
  string country_code = 6;        // ISO 3166-1 alpha-2

  // ✅ GOOD: Reusable message used across multiple parent schemas
  // Address appears in Account, Order, and Contact — defined once, referenced everywhere.
}

message Account {
  string account_id = 1;
  string display_name = 2;

  // ✅ GOOD: Direct field reference to a top-level reusable message
  // One level of nesting via field — clean and shallow.
  Address billing_address = 3;
  Address shipping_address = 4;

  // ❌ BAD: Deep nesting (3+ levels) — generated code becomes unwieldy,
  //          e.g., msg.account.billing.address.city requires deep accessor chains.
  // message NestedOrder {
  //   message LineItem {
  //     message Pricing {
  //       double unit_price = 1;  // Too deep!
  //     }
  //     Pricing pricing = 1;
  //   }
  //   LineItem item = 1;
  // }

  // ✅ GOOD: Flat alternative — extract deeply nested structures to top-level messages.
  repeated OrderLineItem order_items = 5;
}

// Extracted flat message replaces nested structure
message OrderLineItem {
  string product_id = 1;
  int32 quantity = 2;
  UnitPrice pricing = 3;    // Top-level, shallow reference
}

message UnitPrice {
  int64 amount_micros = 1;
  string currency_code = 2;
}

Nesting depth guidelines:

  • Maximum recommended depth: 2 levels (e.g., account.billing_address.city). Beyond that, extract to top-level messages.
  • Reuse over nest: Define shared structures (Address, Money, PhoneNumber) at the package root and reference them by field rather than nesting inline.
  • oneof depth: A oneof can contain nested messages, but prefer flat references for readability.

Constraints

MUST DO

  • Use optional keyword for sparse/nullable fields in proto3; do not use google.protobuf.*Value wrapper types for new APIs
  • Reserve field numbers of removed/deleted fields with reserved N; — never reuse a deleted number within the same package
  • Add deprecated = true to every field before removing it from the schema; keep deprecated fields for at least 2 major versions
  • Apply FIELD_BEHAVIOR_REQUIRED to all fields that must be set before the message can be persisted
  • Apply FIELD_BEHAVIOR_IMMUTABLE to identifiers and keys that cannot change after creation
  • Store monetary values as integer types in the smallest currency unit (micros, cents, satoshis) — never use float or double
  • Validate map key types are integral (int32, int64, uint32, uint64, bool, string) — no enums, floats, or messages as keys
  • Configure Buf lint rules (buf.yaml) and breaking detection before publishing a module version
  • Use reverse-DNS package names (e.g., accounts.v1, payments.v2) to prevent type collisions

MUST NOT DO

  • Define gRPC service or transport-layer definitions in .proto files that are managed by this skill — use grpc-patterns for RPC and service layer design
  • Reuse a field number after deleting it, even if the new field has a different type and meaning
  • Nest messages more than 2 levels deep — extract to top-level package messages instead
  • Use bytes fields for data that should be string — protobuf does not validate encoding at the wire level
  • Store timestamps as int64 epoch seconds or milliseconds — use google.protobuf.Timestamp for timezone safety
  • Put business logic validation rules (e.g., "email must contain @") in proto annotations — proto3 has no built-in value validation; use application-level validators or protoc plugins like protoc-gen-validate

Live References

Authoritative documentation links for Protocol Buffers schema design and tooling.


Related Skills

SkillPurpose
grpc-patternsDesigns gRPC service definitions and transport-layer contracts using the same .proto files — use together when building a full client-server system
data-encodingCovers protobuf binary vs JSON wire format selection, encoding variants, and serialization boundaries — complements this skill's schema design
event-schema-versioningEvent-driven architecture patterns for evolving schemas in pub/sub systems — applies proto3 evolution strategies to event streams
Skills enregistrées