zmdbzero-maintenance data layer
Docs Benchmarks Anti-patterns OpenAPI
Docs / Validation and contracts

protobuf encodeSupported

Supported. protoEncode<T>(value) is replaced at build time with a straight-line proto3 message encoder. The matching protoDecode<T>() is emitted from the same checked TypeIR.

Install the dependency-free call and wire runtime with yarn add @zmdb/protobuf@1.0.0-beta.2; add @zmdb/compiler@1.0.0-beta.2 as a development dependency for the build transform or project compiler. @zmdb/protobuf is not included in the @zmdb/core default install and declares no peer.

Encode a tagged message#

Give every property a stable field number and select an integer width whenever the field is not a protobuf double:

import { protoEncode } from '@zmdb/protobuf';
import { type Proto, type ProtoField } from '@zmdb/schema/tags';

interface UserMessage {
  id: number & Proto<'int32'> & ProtoField<1>;
  name: string & ProtoField<2>;
  deltas: (number & Proto<'sint32'>)[] & ProtoField<3>;
  marker?: number & Proto<'int32'> & ProtoField<4>;
}

const bytes = protoEncode<UserMessage>({
  id: 150,
  name: 'Ada',
  deltas: [-1, 0, 1],
  marker: 0,
});

The transform compiles the type to direct property reads in field-number order. The emitted application imports only the growable wire writer; it does not walk a descriptor or look a field up by name. The returned Uint8Array owns an exact-sized buffer.

An untransformed call throws. A type argument has no runtime representation, so there is no safe fallback that can recover the field numbers.

Integer widths are part of the contract#

An untagged number is a protobuf double. To put an integer on the wire, choose its width and signedness explicitly with Proto<'int32'>, Proto<'uint32'>, Proto<'sint32'>, or a fixed-width spelling. There is no silent int32 default because values above its range would be truncated without a type error.

Every 64-bit integer uses bigint plus an explicit 64-bit tag:

interface Counters {
  signed: bigint & Proto<'int64'> & ProtoField<1>;
  compactNegative: bigint & Proto<'sint64'> & ProtoField<2>;
  unsigned: bigint & Proto<'uint64'> & ProtoField<3>;
}

An untagged bigint is refused because signedness is unknown. A number tagged as a 64-bit integer is refused because it cannot represent the full promised range.

Presence and field order#

A required scalar zero is omitted under proto3 implicit presence. An optional zero is written because the property being present is itself information:

interface RequiredCount {
  count: number & Proto<'int32'> & ProtoField<1>;
}

interface OptionalCount {
  count?: number & Proto<'int32'> & ProtoField<1>;
}

protoEncode<RequiredCount>({ count: 0 }); // Uint8Array []
protoEncode<OptionalCount>({ count: 0 }); // Uint8Array [0x08, 0x00]

Fields are emitted in field-number order, not declaration order. Reordering properties therefore does not change the bytes; changing a released ProtoField<N> does.

Wire mapping#

ProtoField<N> values must be unique within their message, in 1 … 536870911 excluding 19000 … 19999. Invalid or missing field numbers and unsupported scalar choices are build diagnostics.

Interoperability evidence#

The test named produces bytes a reference implementation decodes passes the emitted interop message to protobufjs. Separate fixed vectors produced with protoc 34.2 cover the scalar matrix, packing, proto3 presence, nesting, timestamps and full-width 64-bit extrema. That is evidence for those fixtures, not a claim about unsupported protobuf features.

Limits#

Some shapes are refused rather than guessed: nested arrays have no direct proto3 spelling, optional-nullable fields have three source states but two wire states, discriminated unions have no field-number slot for oneof, and maps remain blocked because the reflector cannot model index signatures. Proto<'bytes'> is also refused until typed-array reflection can carry Uint8Array.

protoDecode<T>() supplies the matching inbound path. It accepts alternate valid field orders and packed/unpacked repeated forms, bounds malformed lengths, and discards unknown fields. Decode-then-re-encode is therefore not suitable for a proxy or relay. The numbering and rollout rules are on Protobuf Messages.

---

See also: Protobuf Messages · Protobuf Decode · AOT Setup