Describe the Bug
A message can use at most one JSON-customizing feature. Each feature's Go generator emits its own top-level MarshalJSON/MarshalJSONSebuf on the message, and two would be a duplicate method declaration, so the generators fail fast via pairwise conflict guards instead of composing. This makes a common, legitimate shape impossible: a message with string-mapped enums (enum_value) and nullable scalars (nullable or empty_behavior). The same exclusion applies between any two of: enum_value, int64_encoding=NUMBER, bytes_encoding, timestamp_format, nullable, empty_behavior, flatten, oneof_config.
To Reproduce
- Create a proto with an enum that carries
(sebuf.http.enum_value) mappings, and a message that uses that enum and a nullable (or empty_behavior) field (see Protobuf Definition below).
- Run
buf generate.
- See the error under Actual Behavior.
Note: this is not fixable by changing the field's enum_encoding. The enum marshaler is triggered by the enum type carrying enum_value mappings, not by the field annotation:
- Dropping
ENUM_ENCODING_STRING leaves the default (still not NUMBER) → enum marshaler still active → same conflict.
- Setting
ENUM_ENCODING_NUMBER → rejected by a separate validation: "field ... has both enum_encoding=NUMBER and enum_value annotations - this is not allowed" (internal/httpgen/enum_encoding.go:19).
Expected Behavior
Both features coexist on one message — type serializes as "call" and delta serializes as null when unset:
{ "type": "call", "delta": null }
Actual Behavior
buf generate aborts:
Failure: plugin protoc-gen-go-http: message OptionsDetails: enum_value requires MarshalJSON
but conflicts with nullable (also requires MarshalJSON) -- only one MarshalJSON-generating
feature is supported per message
(Using empty_behavior instead of nullable yields the identical error with empty_behavior named. protoc-gen-go-client has a mirror guard, internal/clientgen/enum_validation.go:15.)
Environment (please complete the following information):
- OS: macOS 26.6.1 (Apple Silicon / arm64)
- Go version: go1.26.5
- sebuf version: generator built from
main @ d3bcc2a; annotations module buf.build/sebmelki/sebuf commit b5f679ca6c5f4f148c3414adfea268d3
- protoc version: n/a (generating via buf)
- buf version: 1.72.0
Protobuf Definition
syntax = "proto3";
import "sebuf/http/annotations.proto";
enum OptionType {
OPTION_TYPE_UNSPECIFIED = 0;
OPTION_TYPE_CALL = 1 [(sebuf.http.enum_value) = "call"];
OPTION_TYPE_PUT = 2 [(sebuf.http.enum_value) = "put"];
}
message OptionsDetails {
OptionType type = 1; // string-mapped enum
optional double delta = 2 [(sebuf.http.nullable) = true]; // nullable scalar
}
Generated Code (if applicable)
None — generation aborts before emitting. The conflict is that both features would emit the same method on OptionsDetails:
// from the enum_value marshaler (enum_field_encoding.go)
func (x *OptionsDetails) MarshalJSONSebuf(opts protojson.MarshalOptions) ([]byte, error) { /* ... */ }
func (x *OptionsDetails) MarshalJSON() ([]byte, error) { /* ... */ }
// AND from the nullable marshaler (nullable.go) — duplicate declaration on the same type
func (x *OptionsDetails) MarshalJSONSebuf(opts protojson.MarshalOptions) ([]byte, error) { /* ... */ }
func (x *OptionsDetails) MarshalJSON() ([]byte, error) { /* ... */ }
Command Used
Additional Context
Root cause (paths at main @ d3bcc2a):
- Each feature emits its own
MarshalJSON/MarshalJSONSebuf: internal/httpgen/empty_behavior.go:131, plus nullable.go, enum_field_encoding.go, encoding.go (int64), bytes_encoding.go, timestamp_format.go, flatten.go, oneof_discriminator.go.
- Pairwise fail-fast guards:
enum_field_encoding.go:261 checkEnumMarshalJSONConflict, mirrored in encoding.go:211, flatten.go, oneof_discriminator.go.
- Enum trigger is the mapping on the enum type:
enum_field_encoding.go:49 customEnumForField.
Workaround: for a message that needs string-mapped enums, keep nullable scalars as google.protobuf.DoubleValue/Int32Value wrappers — protojson renders unset wrappers as null natively without a message-level MarshalJSON, so they don't hit the guard.
Describe the Bug
A message can use at most one JSON-customizing feature. Each feature's Go generator emits its own top-level
MarshalJSON/MarshalJSONSebufon the message, and two would be a duplicate method declaration, so the generators fail fast via pairwise conflict guards instead of composing. This makes a common, legitimate shape impossible: a message with string-mapped enums (enum_value) and nullable scalars (nullableorempty_behavior). The same exclusion applies between any two of:enum_value,int64_encoding=NUMBER,bytes_encoding,timestamp_format,nullable,empty_behavior,flatten,oneof_config.To Reproduce
(sebuf.http.enum_value)mappings, and a message that uses that enum and anullable(orempty_behavior) field (see Protobuf Definition below).buf generate.Note: this is not fixable by changing the field's
enum_encoding. The enum marshaler is triggered by the enum type carryingenum_valuemappings, not by the field annotation:ENUM_ENCODING_STRINGleaves the default (still notNUMBER) → enum marshaler still active → same conflict.ENUM_ENCODING_NUMBER→ rejected by a separate validation: "field ... has both enum_encoding=NUMBER and enum_value annotations - this is not allowed" (internal/httpgen/enum_encoding.go:19).Expected Behavior
Both features coexist on one message —
typeserializes as"call"anddeltaserializes asnullwhen unset:{ "type": "call", "delta": null }Actual Behavior
buf generateaborts:(Using
empty_behaviorinstead ofnullableyields the identical error withempty_behaviornamed.protoc-gen-go-clienthas a mirror guard,internal/clientgen/enum_validation.go:15.)Environment (please complete the following information):
main@d3bcc2a; annotations modulebuf.build/sebmelki/sebufcommitb5f679ca6c5f4f148c3414adfea268d3Protobuf Definition
Generated Code (if applicable)
None — generation aborts before emitting. The conflict is that both features would emit the same method on
OptionsDetails:Command Used
Additional Context
Root cause (paths at
main@d3bcc2a):MarshalJSON/MarshalJSONSebuf:internal/httpgen/empty_behavior.go:131, plusnullable.go,enum_field_encoding.go,encoding.go(int64),bytes_encoding.go,timestamp_format.go,flatten.go,oneof_discriminator.go.enum_field_encoding.go:261checkEnumMarshalJSONConflict, mirrored inencoding.go:211,flatten.go,oneof_discriminator.go.enum_field_encoding.go:49customEnumForField.Workaround: for a message that needs string-mapped enums, keep nullable scalars as
google.protobuf.DoubleValue/Int32Valuewrappers — protojson renders unset wrappers asnullnatively without a message-levelMarshalJSON, so they don't hit the guard.