Skip to content

About

proto-lock wraps gnostic-grpc and pins every protobuf field number to a field name permanently — the same way `package-lock.json` pins dependency versions. Generate your `.proto` from an OpenAPI spec as often as you like; your wire format never silently breaks.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

proto-lock

Stable field numbers for OpenAPI → gRPC generation.

proto-lock wraps gnostic-grpc and pins every protobuf field number to a field name permanently — the same way package-lock.json pins dependency versions. Generate your .proto from an OpenAPI spec as often as you like; your wire format never silently breaks.


The problem

gnostic-grpc assigns protobuf field numbers sequentially based on the order fields appear in your OpenAPI spec:

message Product {
  string id = 1;
  string name = 2;
  double price = 3;
}

Field numbers are the contract in gRPC. They — not field names — are what gets encoded on the wire. So the moment you remove or reorder a field, every number below it shifts:

// after deleting `name` and regenerating:
message Product {
  string id = 1;
  double price = 2;   // ⚠️ was 3 — every deployed client now misreads this field
}

Nothing errors. Nothing warns you. Old clients keep sending price as field 3, new servers read field 3 as nothing, and field 2 — which clients still think is name — is now silently interpreted as price. In a running gRPC system this is a silent data-corruption bug in production.

The fix

proto-lock maintains a .proto-lock file that maps field names to field numbers and never lets a number change once assigned:

  • A field keeps its number forever, regardless of spec order.
  • A deleted field's number is marked reserved and permanently retired — never reused.
  • A new field always gets max(existing numbers) + 1.

The lock file is committed to version control alongside your code, just like package-lock.json. Your .proto output becomes deterministic and backward-compatible across every regeneration.


Installation

npm install -g proto-lock

proto-lock shells out to gnostic and its gRPC plugin, which you must install separately (they're Go binaries):

go install github.com/google/gnostic/cmd/gnostic@latest
go install github.com/google/gnostic-grpc@latest

Under the hood proto-lock runs gnostic --grpc-out=<dir> <spec> (gnostic discovers the gnostic-grpc plugin via the --grpc-out flag and writes a .proto into that directory). Make sure both gnostic and gnostic-grpc are on your PATH. proto-lock checks for gnostic on startup and tells you how to install it if it's missing.

Requirements: Node.js 18+, Go (for gnostic + the gRPC plugin).


Usage

proto-lock has a single command: generate.

First run — no lock file yet

proto-lock generate --spec openapi.yaml --out service.proto

This will:

  1. Validate that openapi.yaml exists and is a valid OpenAPI spec.
  2. Run gnostic --grpc-out to produce a raw .proto.
  3. Assign field numbers sequentially (1, 2, 3, …).
  4. Write service.proto with stable field numbers.
  5. Write a proto.lock file alongside the output.

Commit proto.lock to version control.

--out may be a file path (service.proto) or a directory (./ or out/). If you pass a directory, the proto is written inside it with a name derived from the spec — e.g. sample.openapi.yaml → sample.proto.

Subsequent runs — respect the existing lock

proto-lock generate --spec openapi.yaml --out service.proto --lock proto.lock

This will:

  1. Validate the spec and read the existing proto.lock.
  2. Run gnostic --grpc-out to produce a raw .proto.
  3. For each field in the new output:
    • If it exists in the lock → reuse its pinned number.
    • If it's new → assign the next available number.
  4. For each field in the lock that's no longer in the output → mark it reserved: true.
  5. Write service.proto with stable field numbers.
  6. Write the updated proto.lock.

Lock file format

proto.lock is plain JSON and meant to be read in diffs and committed to git:

{
  "version": 1,
  "generatedAt": "2026-06-09",
  "messages": {
    "Product": {
      "fields": {
        "id":        { "number": 1, "reserved": false },
        "name":      { "number": 2, "reserved": false },
        "old_price": { "number": 3, "reserved": true }
      }
    }
  }
}

Rules:

Rule Meaning
reserved: true The field is gone from the spec. Its number is retired and never handed to a different field. If the same field name returns later, it reclaims its original number.
New fields Always get max(existing numbers) + 1 — so renaming id → barathid gives barathid a fresh number and retires id's.
Source of truth Messages and fields come from the proto gnostic generates (e.g. GetProductRequest from an operation), not from components.schemas directly.
After every run The lock file is written/updated.
Version control Commit the lock file, like package-lock.json.

If the lock file is corrupted, proto-lock will warn you. Deleting it regenerates from scratch — but field numbers will reset, so only do this if no clients depend on the current wire format.


Why this exists

We hit this exact bug migrating a large ecommerce platform to gRPC, using OpenAPI specs as the source of truth via gnostic-grpc. A routine field removal shifted numbers across a core message and corrupted data between services that hadn't redeployed yet. The generated proto looked fine; the bug was in the numbers, which no code review catches by eye.

proto-lock is the tool we built to make that class of bug impossible. It's small on purpose: one lock file, one command, no config.


What proto-lock does not do (v1)

  • No config file support.
  • No watch mode.
  • No programmatic Node.js API.
  • No GUI / web interface.
  • No plugin system.
  • No proto → OpenAPI (reverse direction).
  • No automatic gnostic-grpc installation.

License

MIT

About

proto-lock wraps gnostic-grpc and pins every protobuf field number to a field name permanently — the same way `package-lock.json` pins dependency versions. Generate your `.proto` from an OpenAPI spec as often as you like; your wire format never silently breaks.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages