Skip to content

codegen: oneof_struct_field_attribute for the struct field holding a oneof - #561

Draft
benedikt-bartscher wants to merge 4 commits into
anthropics:mainfrom
benedikt-bartscher:feat/oneof-field-attribute
Draft

benedikt-bartscher wants to merge 4 commits into
anthropics:mainfrom
benedikt-bartscher:feat/oneof-field-attribute

Conversation

@benedikt-bartscher

@benedikt-bartscher benedikt-bartscher commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

field_attribute never reaches the struct field that holds a oneof, the Option<OneofEnum> member. On the path of the oneof it matches only the variants (.pkg.Msg.oneof.variant), so that field cannot carry an attribute of its own. The case that needs one is serde: #[serde(skip_serializing_if = "Option::is_none")] belongs on the struct field, and serde rejects it on enum variants. prost-build puts a field_attribute on the path of a oneof on the struct field and on every variant, so a project moving from prost loses that attribute with no replacement.

This adds Config::oneof_struct_field_attribute(path, attribute) / CodeGenConfig::oneof_struct_field_attributes. It is matched against the path of the oneof like oneof_attribute and applies to the field of the owned struct only. A variant path does not match the oneof, so a rule written with one has no effect.

Changes:

  • Codegen: message.rs emits the matching attributes on the oneof field, after the keyword-rename doc note and the #[serde(flatten)] that buffa adds. View structs get none, like the rest of the attribute family. A malformed attribute fails with InvalidCustomAttribute, as for the other lists. A #[deprecated] given through the option puts #[allow(deprecated)] on the generated impls that read the field, as one from field_attribute does for a plain field.
  • buffa-build: oneof_struct_field_attribute, with an example, a map of which of the three attribute methods reaches which generated line, and a Pitfalls section for generate_json. The example turns unknown-field preservation off, because UnknownFields does not implement serde::Serialize. The field_attribute and oneof_attribute docs point to the new method.
  • Docs: the attribute row in docs/guide.md, and the field_attribute row in docs/migration-from-prost.md, which said "Same API" and now names the difference on the path of a oneof.
  • Tests: the codegen tests parse the generated file and assert the exact attribute list on the oneof field, for exact, prefix and catch-all rules, a nested message with JSON and views, a proto3 optional field beside a real oneof, and two rules on one field. buffa-test compiles a caller-derived serde::Serialize with skip_serializing_if on the oneof field, and a oneof field deprecated through the option.
  • Changelog: an "Added" fragment.

On the name: unbox_oneof_in and field_attribute take the path of a member of a oneof, so "oneof field" reads as a member. oneof_struct_field_attribute names the struct field that holds the oneof.

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

field_attribute never reaches the struct's Option<OneofEnum> field: on
the oneof's path it matches only the variants, where an attribute such as
#[serde(skip_serializing_if = "...")] is rejected. prost-build put such a
field_attribute on the struct field as well.
@benedikt-bartscher
benedikt-bartscher force-pushed the feat/oneof-field-attribute branch from eeb3bdd to 0b99080 Compare October 4, 2026 23:00
@iainmcgin

Copy link
Copy Markdown
Collaborator

[claude code] Thanks for this, and for raising the naming question in the description. Yes, please rename it: Config::oneof_struct_field_attribute and CodeGenConfig::oneof_struct_field_attributes.

The reason is the one you gave. unbox_oneof_in already takes the path of a oneof member, so oneof_field_attribute(".pkg.Event.payload.text", "#[serde(skip)]") is an easy call to write for "an attribute on a field of a oneof". That rule matches no oneof, generates nothing, and the build succeeds without a warning. The longer name says which field it means.

The branch also conflicts with main since #521, in buffa-codegen/src/message.rs where the oneof struct field is emitted. When you rebase, keep the #rename_note that #521 added there.

With the rename and the rebase it is ready for a full review.

# Conflicts:
#	buffa-codegen/src/message.rs
`oneof_field_attribute` reads as "an attribute on a oneof's member field", which is what the paths given to `unbox_oneof_in` mean (`.pkg.Msg.my_oneof.variant`). A rule written with such a member path matches no oneof and silently does nothing. `Config::oneof_struct_field_attribute` and `CodeGenConfig::oneof_struct_field_attributes` name the target instead: the message struct's field that holds the oneof, matched by the oneof's own path.
A `#[deprecated]` given through `oneof_struct_field_attribute` now puts
`#[allow(deprecated)]` on the generated impls that visit the oneof's
struct field, as a `#[deprecated]` from `field_attribute` does for a
plain field. Output is unchanged when no such rule exists.

The codegen tests parse the generated files and assert the exact
attribute list on the oneof field: placement next to a plain field,
order after `serde(flatten)` and after the keyword-rename doc note,
prefix and catch-all rules, segment-boundary and variant-path
non-matches, a proto3 `optional` field beside a real oneof, rule
accumulation order, and the error for a malformed attribute.

buffa-test compiles two fixtures: a oneof whose struct field is
deprecated through the option, with views, lazy views, text, JSON and
reflection on, and a message with a caller-derived `serde::Serialize`
whose oneof field takes `skip_serializing_if`.

The method doc maps `oneof_struct_field_attribute`, `oneof_attribute`
and `field_attribute` to the generated lines they reach, states that a
variant path has no effect, and adds a Pitfalls section for
`generate_json` and feature-gated impls. Its example turns
unknown-field preservation off, because `UnknownFields` does not
implement `serde::Serialize`.
@iainmcgin iainmcgin changed the title codegen: oneof_field_attribute for the struct field holding a oneof codegen: oneof_struct_field_attribute for the struct field holding a oneof Oct 10, 2026
@iainmcgin

Copy link
Copy Markdown
Collaborator

[claude code] To close this out sooner, the rename asked for above is now on this branch, together with a merge of main and the fixes from a full review. Your commit 0b990808 is unchanged; three commits sit on top of it.

b324740d: merge of main. One conflict, in buffa-codegen/src/message.rs: #521 added a doc note for a keyword-renamed oneof to the same struct field. The resolution keeps your for loop, and the field carries the doc note first, then serde(flatten), then the custom attributes.

2ea4b4c7: rename. oneof_field_attribute is now oneof_struct_field_attribute, and the config field is oneof_struct_field_attributes, across the seven files of the PR.

f2cba89a: review fixes.

  • A #[deprecated] given through the option now puts #[allow(deprecated)] on the generated impls that read the field, as field_attribute does for a plain field. Without it, a buffa-test fixture with such a rule failed clippy with 16 use of deprecated field errors. Output is unchanged when there is no such rule.
  • The rustdoc example did not compile for a user: a derived serde::Serialize with generate_json off fails with E0277: the trait bound UnknownFields: serde::Serialize is not satisfied. The example now calls .preserve_unknown_fields(false), and buffa-test compiles it.
  • The two placement tests measured byte distance and passed with the attribute on the struct. They now parse the generated file and assert the exact attribute list on the oneof field. New tests cover the malformed-attribute error, prefix and catch-all rules, a segment-boundary and a variant-path non-match, a proto3 optional field beside a real oneof, the keyword-rename doc note, and two rules on one field.
  • The method doc maps the three attribute methods to the generated lines they reach, says that a variant path has no effect, and has a Pitfalls section for generate_json. The view field stays unmarked under #[deprecated], and the docs say so.
  • The changelog fragment file is renamed to match.

The prost parity claim holds in prost-build 0.14.4: code_generator.rs:615 puts the attribute on the struct field and :680 on each variant. The PR title and description now use the new name.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants