# 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.

- Skill: `paulpas/protobuf-format` (Agent Skill)
- Install (CLI): `npx skillmds@latest add paulpas/protobuf-format`
- Raw SKILL.md: https://api.skillmd.com/api/skills/paulpas/protobuf-format/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: paulpas (https://skillmd.com/u/paulpas)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/paulpas/protobuf-format

---






# 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.

```protobuf
// 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:**

| Scenario | Choose | JSON Serialization | Why |
|----------|--------|--------------------|-----|
| UTC date-time | `google.protobuf.Timestamp` | `"2025-01-15T10:30:00Z"` | ISO 8601 with timezone; native datetime in generated code |
| Time span | `google.protobuf.Duration` | `"3600.5s"` | Human-readable with nanosecond precision |
| Dynamic key-value | `google.protobuf.Struct` + `Value` | `{"key": "value", "num": 42}` | Full JSON type mapping; use sparingly — loses schema enforcement |
| Partial update fields | `google.protobuf.FieldMask` | `["field_a", "nested.field_b"]` | Standardized for PATCH operations |
| Wrap unknown message | `google.protobuf.Any` | `{"@type": "...", "field": "val"}` | Type URL + serialized payload; expensive and loses type safety |
| Empty placeholder | `google.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.

```protobuf
// 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:**

| Property | `optional string name = 2;` | `google.protobuf.StringValue name = 2;` |
|----------|-----------------------------|----------------------------------------|
| Generated code type | `name: 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 unset | No bytes emitted (same as absent field) | `Empty` message serialized (tiny overhead) |
| Code complexity | Native nullable type | Message 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.

```protobuf
// 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.

```protobuf
// 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:**

| Data | Best Type | Reason |
|------|-----------|--------|
| UUIDs, SKUs, email addresses | `string` | External sources are strings; varint-encoding doesn't help |
| Internal sequential IDs | `int64` | Compact varint encoding; monotonically increasing |
| Monetary amounts | `int64` (smallest unit) | Exact precision; no floating point issues |
| Booleans | `bool` | Single bit on wire |
| Counts, quantities | `int32` unless exceeding ±2B | Smaller wire size than int64 |
| Large counters, timestamps in seconds | `int64` | Exceeds int32 range; 64-bit varint is still efficient for small values |
| Enumerated categories | `enum` | Self-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.

```protobuf
// 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.

- [Protocol Buffers Language Guide (proto3)](https://protobuf.dev/reference/protobuf/proto3/) — Official proto3 language reference covering all syntax elements
- [Well-Known Types Reference](https://protobuf.dev/reference/protobuf/googleapis/) — Complete reference for `google.protobuf.*` types with JSON mapping tables
- [Google API Annotations](https://github.com/googleapis/google-api-common-protos) — Field behavior, resource, and custom option annotations used in production schemas
- [Buf CLI Documentation](https://docs.buf.build/cli-documentation) — Modern Protobuf build tool: `buf lint`, `buf breaking`, `buf generate`
- [Buf Schema Registry (BSR)](https://docs.buf.build/bscan/introduction) — Centralized registry for versioning, sharing, and distributing `.proto` modules
- [protoc-gen-validate](https://github.com/bufbuild/protoc-gen-validate) — Validation library for proto3 with declarative constraints on fields
- [Protocol Buffers Best Practices (Google)](https://protobuf.dev/programming-guides/api/) — Google's own guidance on API design with Protobuf, including naming and evolution conventions

---

## Related Skills

| Skill | Purpose |
|---|---|
| `grpc-patterns` | Designs gRPC service definitions and transport-layer contracts using the same `.proto` files — use together when building a full client-server system |
| `data-encoding` | Covers protobuf binary vs JSON wire format selection, encoding variants, and serialization boundaries — complements this skill's schema design |
| `event-schema-versioning` | Event-driven architecture patterns for evolving schemas in pub/sub systems — applies proto3 evolution strategies to event streams |

