Describe the desired outcome from the user's perspective
As a Zilla operator, I want the schema codecs to retain and expose whatever field annotations my schemas already carry, so that any consumer of a compiled schema — a model extension, a registry-mode catalog integration, or similar — can read them off the field object without the codec silently discarding whatever it doesn't recognize as a standard keyword.
Acceptance criteria
1. Retain unrecognised field annotations
The codecs discard them today.
common-avro — AvroSchemaCompiler keeps only logicalType, precision, scale, aliases, name, size; AvroField exposes only name(), type(), aliases(), defaultValue()
common-json — equivalent gap for JSON Schema annotations
common-protobuf — ProtobufField retains no options at all (its fields are number, name, jsonName, type, repeated, required, packed, proto3Optional, typeName, oneofName, defaultValue, message, enumeration). ProtobufSourceCompiler.applyOptions consumes the known ones and discards the rest
2. Annotations are read from the field, not the type
Read from the field object only, for every format. A tag on the field's type is ignored, named type references are not followed, and record-level and root-level tags are not inherited. This is a deliberate scope boundary, not an oversight: supporting type-level or inherited tags would require walking type references and merging scopes across them, machinery this issue does not need — a schema author who wants a type's every usage tagged can tag each field directly.
So:
- Avro — field object only.
- JSON Schema — the property's schema, necessarily, since there is no separate field object
- Protobuf — field options only
3. Per-format exposure and per-format evaluation
No shared evaluator. A model is one format at a time, so there is no polymorphic call site to serve, and forcing a common intermediate representation costs more than it saves:
- Avro and JSON Schema annotations are already
JsonValue — Avro schemas are JSON, JSON Schema annotations likewise
- Protobuf options come from an ANTLR parse tree of text-format constants, an entirely different source.
ProtobufSourceCompiler captures option.optionName().getText() and option.constant().getText() with no descriptor resolution
So each codec exposes annotations in the form natural to it:
// common-avro, common-json
JsonValue attribute(String name); // null when absent
For common-protobuf, expose the option constant from the parse tree rather than converting it. Do not route through ProtobufJson — that bridge converts protobuf wire data to JSON, whereas these are text-format source constants, which is a different problem. Walking the grammar's constant subtree is the right approach.
Two protobuf details that follow:
- Options need no pre-declaration. Because parsing is source-text with our own grammar and never resolves a descriptor, an arbitrary
[(acme.classification) = "PII"] is readable even though nothing declares that extension. Document that the producer's protoc toolchain will still reject an undeclared option at their build time
- No proto3 JSON
int64→string quirk. Reading from source text, [(acme.limit) = 123] is an integer constant, so it compares as a number. That quirk only applied under the ProtobufJson route
- String constants come back from
getText() with their quotes — unquote before comparing
Also needed alongside field-level retention, since a consumer walking a schema for tagged fields needs to discover which paths carry annotations without re-parsing: a predicate-driven path-discovery method per format (AvroType.matchingPaths(Predicate), JsonSchema.matchingPaths(Predicate), ProtobufMessage.matchingPaths(Predicate)), returning RFC 6901-style pointers for every node a caller-supplied predicate matches.
Coverage
- round-trip: a schema declaring annotations of each shape compiles, exposes them, and survives the existing conformance suites
- protobuf: an option not declared by any imported
.proto is still retained and readable
- protobuf: quoted string constants unquote correctly; message-valued options expose their structure rather than flattened text
Why configurable rather than a name we pick. Field tagging has no clear winner — Snowplow's bare pii: true, Adobe XDM's xdm: namespace, assorted x-pii/x-data-classification customs, and Apicurio #7879 building field-level classification with authorization-scoped views. Hardcoding one couples generic code to a vendor; minting our own forks the space and forces adopters to re-tag. This issue only retains and exposes whatever a schema already carries — it declares no vocabulary of its own, and no matching/selection semantics on top of it. What a consumer does with an exposed annotation is out of scope here.
Additional context
Scope note (narrowed from the original issue text): this issue originally also proposed, as §§4–9, a shared tagged:/named: field-matcher config DSL with a normative matcher matrix, an overlay: interaction, and a root-level (schema-root) attribute() accessor — all evaluated generically across model-avro/model-json/model-protobuf. None of that materialized as a shared oss capability, and none currently has a driving consumer:
Retitled and re-scoped to reflect the part that actually shipped, is actually consumed, and stays business-agnostic per this repo's own rule against building shared-component surface for a hypothetical future consumer. Fixed by #2368.
Describe the desired outcome from the user's perspective
As a Zilla operator, I want the schema codecs to retain and expose whatever field annotations my schemas already carry, so that any consumer of a compiled schema — a model extension, a registry-mode catalog integration, or similar — can read them off the field object without the codec silently discarding whatever it doesn't recognize as a standard keyword.
Acceptance criteria
1. Retain unrecognised field annotations
The codecs discard them today.
common-avro—AvroSchemaCompilerkeeps onlylogicalType,precision,scale,aliases,name,size;AvroFieldexposes onlyname(),type(),aliases(),defaultValue()common-json— equivalent gap for JSON Schema annotationscommon-protobuf—ProtobufFieldretains no options at all (its fields are number, name, jsonName, type, repeated, required, packed, proto3Optional, typeName, oneofName, defaultValue, message, enumeration).ProtobufSourceCompiler.applyOptionsconsumes the known ones and discards the rest2. Annotations are read from the field, not the type
Read from the field object only, for every format. A tag on the field's type is ignored, named type references are not followed, and record-level and root-level tags are not inherited. This is a deliberate scope boundary, not an oversight: supporting type-level or inherited tags would require walking type references and merging scopes across them, machinery this issue does not need — a schema author who wants a type's every usage tagged can tag each field directly.
So:
3. Per-format exposure and per-format evaluation
No shared evaluator. A model is one format at a time, so there is no polymorphic call site to serve, and forcing a common intermediate representation costs more than it saves:
JsonValue— Avro schemas are JSON, JSON Schema annotations likewiseProtobufSourceCompilercapturesoption.optionName().getText()andoption.constant().getText()with no descriptor resolutionSo each codec exposes annotations in the form natural to it:
For
common-protobuf, expose the option constant from the parse tree rather than converting it. Do not route throughProtobufJson— that bridge converts protobuf wire data to JSON, whereas these are text-format source constants, which is a different problem. Walking the grammar'sconstantsubtree is the right approach.Two protobuf details that follow:
[(acme.classification) = "PII"]is readable even though nothing declares that extension. Document that the producer's protoc toolchain will still reject an undeclared option at their build timeint64→string quirk. Reading from source text,[(acme.limit) = 123]is an integer constant, so it compares as a number. That quirk only applied under theProtobufJsonroutegetText()with their quotes — unquote before comparingAlso needed alongside field-level retention, since a consumer walking a schema for tagged fields needs to discover which paths carry annotations without re-parsing: a predicate-driven path-discovery method per format (
AvroType.matchingPaths(Predicate),JsonSchema.matchingPaths(Predicate),ProtobufMessage.matchingPaths(Predicate)), returning RFC 6901-style pointers for every node a caller-supplied predicate matches.Coverage
.protois still retained and readableWhy configurable rather than a name we pick. Field tagging has no clear winner — Snowplow's bare
pii: true, Adobe XDM'sxdm:namespace, assortedx-pii/x-data-classificationcustoms, and Apicurio #7879 building field-level classification with authorization-scoped views. Hardcoding one couples generic code to a vendor; minting our own forks the space and forces adopters to re-tag. This issue only retains and exposes whatever a schema already carries — it declares no vocabulary of its own, and no matching/selection semantics on top of it. What a consumer does with an exposed annotation is out of scope here.Additional context
Scope note (narrowed from the original issue text): this issue originally also proposed, as §§4–9, a shared
tagged:/named:field-matcher config DSL with a normative matcher matrix, anoverlay:interaction, and a root-level (schema-root)attribute()accessor — all evaluated generically acrossmodel-avro/model-json/model-protobuf. None of that materialized as a shared oss capability, and none currently has a driving consumer:attribute()/option()/matchingPaths()), and explicitly scoped tag-based field selection as "entirely its own design" for whatever extension installs it — this issue "declares no opinion on what any installed extension is for."AvroModelExtFieldMatcherConfig/*SelectConfig/*SelectTypeand the json/protobuf equivalents) directly inside the commercial ext modules, rather than depending on a shared oss primitive.attribute()accessor has no consumer anywhere in zilla or zilla-plus today.Retitled and re-scoped to reflect the part that actually shipped, is actually consumed, and stays business-agnostic per this repo's own rule against building shared-component surface for a hypothetical future consumer. Fixed by #2368.