Schema languages¶
Both are first class. JSON Schema is the natural fit for CloudEvents' JSON payloads; protobuf descriptors matter because gRPC services in this estate would register too, and a registry serving only one would send the other somewhere else.
The cost, stated: everything language-specific doubles. The buf schema registry exists in large part
because Confluent's serves Avro well and leaves protobuf users hand-sharing .proto files, so
somebody else has already paid for this lesson.
JSON Schema¶
schema.JSONSchema. Bytes is the schema document. Root is unused — a JSON Schema is its own
root.
schema.Schema{
URN: urn.Schema{Name: "orders.created", Version: 3},
Language: schema.JSONSchema,
Bytes: []byte(`{"type":"object","required":["order"]}`),
}
Validation is what you would expect: required fields, types, minimum, additionalProperties, the
lot. A payload that is not JSON is ErrInvalidPayload; a schema document that will not compile is
ErrSchemaUnusable, because the two have different owners and land on different sides of the
guard's fail-open line.
References outside the schema are refused. The compiler's default loader resolves a file: URL
by reading local disk, so {"$ref":"file:///etc/passwd"} would make validation depend on the
filesystem of whichever replica handled the message — two replicas then disagreeing about one
message, which is the non-reproducibility immutability exists to prevent. A schema is self-contained
here.
Compiled schemas are cached by identifier and by a digest of the bytes. The identifier alone would be wrong: immutability is a property of one store, while a validator is a process-wide singleton fed by any resolver — an embedded register and a network client in the same process, or a test fake.
Protobuf¶
schema.Protobuf. Bytes is a serialised FileDescriptorSet. Root is required: it names the
fully-qualified message a payload is.
schema.Schema{
URN: urn.Schema{Name: "orders.shipped", Version: 1},
Language: schema.Protobuf,
Bytes: descriptorSet,
Root: "uk.phpboyscout.OrderShipped",
}
A descriptor set holds every message the target file depends on, and nothing in it says which one a payload is. Inferring the entry point by convention — the first message of the last file, say — is wrong exactly once, and then validates against a different message than anybody meant.
A missing root is ErrSchemaUnusable, so a guard fails open on it: a schema published without
its root is our mistake, not the producer's. The cache is keyed on the root as well as the bytes,
because the root is what selects the message.
Produce the descriptor set with protoc:
--include_imports is not optional: without it the set is missing the types the target message
depends on and will not resolve.
What the protobuf validator actually proves¶
Less than you would assume
Protobuf's wire format is deliberately permissive, and the validator inherits that:
| Payload | Result |
|---|---|
| Empty | Valid. Every proto3 field is optional and takes its zero value |
| Contains an unknown field | Valid. Unknown fields are preserved, by design |
| A field whose wire type does not match the descriptor | Valid. It is treated as an unknown field and skipped |
| Truncated | Invalid |
| Trailing bytes that do not decode | Invalid |
What is left is a real check on framing, and it is weaker than JSON Schema's because the format is weaker. Each of those three permissions is asserted in the test suite rather than left to be discovered.
If you need field-level guarantees for protobuf traffic, they belong in the application, not here.
Versions¶
orders.created:3, never :v3.1.0 and never :2026-08-28.
An integer is unambiguous, orders trivially, and claims nothing. Semantic versioning would imply a compatibility promise that nothing checks, and a date encodes when rather than what.
A version bump today asserts only that the bytes differ. It is not a compatibility claim, and nothing should read it as one.