Programming · free software · the open webSubscribe via RSS

Changing a Cap’n Proto schema without renumbering history

Cap’n Proto field ordinals preserve a schema’s evolution history. Adding a field can keep old messages readable, provided the new declaration respects ordinal ordering and defaults.

Ordinals record when fields arrived

A Cap’n Proto schema is compiled into code that manipulates a strongly typed message format. A field’s ordinal is more than a local label: it records its place in the type’s evolution. Cap’n Proto requires fields to be numbered consecutively, starting at zero, in the order they were added [1].

Consider a first version of Note with title @0 :Text and body @1 :Text. A later version can add revision @2 :UInt32. Assigning the new field the next ordinal preserves the history: older readers know the first two fields, while newer readers can also interpret the third. Reusing an ordinal after removing a field would confuse that history, because old data at that position could be given a different meaning.

What the schema means

The declaration shows a file identifier followed by a Note struct with three fields. title and body are the original text fields at ordinals zero and one. revision is a later unsigned 32-bit field at ordinal two. The comment describes the expected value when an older message has no stored value for the added field.

This is a wire-format evolution question as well as a generated-code question. A newly generated reader has an accessor for revision; a reader using the earlier schema has no such accessor and does not gain the field merely because it is present in a message. Conversely, an old message contains no revision value for the new reader to retrieve, so the schema’s default behavior matters.

@0xc8a5b4e39d7216f0;

struct Note {
  title @0 :Text;
  body @1 :Text;
  # Added later. Old messages read this field as zero.
  revision @2 :UInt32;
}
Illustrative schema · Download example

Defaults and unknown fields

For a newly added scalar field, the reader needs a defined value when reading data written before that field existed. The example uses zero as the expected value. Application logic should account for that possibility: if zero means “not yet assigned,” it can serve as the absence value; if zero is a meaningful revision, the schema or application needs another way to distinguish absence from an explicit zero.

An older reader can process the fields it knows about without understanding the later field. The important compatibility property is that the new field occupies its own ordinal and does not reinterpret existing data. At the application layer, however, compatibility still depends on what the two versions mean by the known fields and on whether the older generated code preserves unknown data when it reads and rewrites a message.

Unions need the same care

Cap’n Proto unions let multiple fields share storage while a separate tag identifies which member is active. Their members use the same ordinal space as the containing struct’s fields, so adding union cases also changes a schema’s evolution history. A reader that does not know a newer case cannot safely treat it as an older case just because the storage overlaps [1].

When a schema changes, review both the wire interpretation and the generated API that application code consumes. Keep existing ordinals attached to their original meanings, add new fields in sequence, and choose defaults that make sense for data written before the change. Those practices let versions evolve without asking old bytes to tell a different story.

References

  1. Cap’n Proto schema language

Suggest a correction · All posts