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.
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.
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
reservedand 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.
npm install -g proto-lockproto-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@latestUnder 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).
proto-lock has a single command: generate.
proto-lock generate --spec openapi.yaml --out service.protoThis will:
- Validate that
openapi.yamlexists and is a valid OpenAPI spec. - Run
gnostic --grpc-outto produce a raw.proto. - Assign field numbers sequentially (
1, 2, 3, …). - Write
service.protowith stable field numbers. - Write a
proto.lockfile alongside the output.
Commit proto.lock to version control.
--outmay be a file path (service.proto) or a directory (./orout/). 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.
proto-lock generate --spec openapi.yaml --out service.proto --lock proto.lockThis will:
- Validate the spec and read the existing
proto.lock. - Run
gnostic --grpc-outto produce a raw.proto. - 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.
- For each field in the lock that's no longer in the output → mark it
reserved: true. - Write
service.protowith stable field numbers. - Write the updated
proto.lock.
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.
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.
- 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.
MIT