Skip to content

Retain and expose schema field annotations from Avro, JSON Schema, and Protobuf codecs #2302

Description

@jfallows

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-avroAvroSchemaCompiler keeps only logicalType, precision, scale, aliases, name, size; AvroField exposes only name(), type(), aliases(), defaultValue()
  • common-json — equivalent gap for JSON Schema annotations
  • common-protobufProtobufField 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions