Skip to content

Commit 0aad7b8

Browse files
fix(enum): apply enum_value custom strings on Go server & client JSON (#214)
* fix(enum): apply enum_value custom strings on Go server & client JSON The OpenAPI generator (and TS/Python clients) honor (sebuf.http.enum_value) and emit "low"/"medium"/"high", but the generated Go HTTP server emitted the raw proto names ("RISK_LEVEL_LOW"). Both Go surfaces serialize at the message level with protojson, which never invokes the Go enum type's MarshalJSON — so the enum-type marshalers in *_enum_encoding.pb.go are dead code server-side. Add a message-level marshaler (*_enum_field_encoding.pb.go) for any message with a custom-enum field. It reuses the existing xToJSON/xFromJSON lookup maps to rewrite enum fields between proto value names and custom strings, in both directions (MarshalJSONSebuf emits "low"; UnmarshalJSONSebuf accepts "low"), across all shapes: singular, proto3 optional, repeated, and map<_, enum>. Because a Go type can own only one MarshalJSON, register enum_value in the existing fail-fast conflict web (flatten, oneof) plus a new checkEnumMarshalJSONConflict, so combining a custom enum with another JSON-mapping annotation errors clearly instead of emitting duplicate methods. Mirrored in clientgen for Go-client request parity. Adds examples/enum-encoding with an end-to-end test proving the wire format. Also regenerates the TimestampFormat OpenAPI golden, which was already stale on current deps from a recent protobuf bump (unrelated to this change). Follow-ups tracked in #213 (transitive nesting, generator composition, dual-plugin file collision, cross-package enums). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(lint): suppress dupl on generated enum marshaler helpers golangci-lint flags the message-level marshaler/unmarshaler generators as duplicates of the flatten/timestamp equivalents. Add //nolint:dupl where dupl actually fires (matching the existing bytes_encoding.go convention), placed asymmetrically to avoid an unused-directive nolintlint error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(enum): handle UseProtoNames keys and fail loudly on cross-package enums Addresses Codex review feedback on the enum_value marshaler: 1. UseProtoNames regression: the patcher keyed only on the camelCase JSON name, so a server/client configured with protojson UseProtoNames (snake_case keys) left enum fields unpatched and leaked raw proto names. Marshal/unmarshal now patch both the JSON name and the proto name key. 2. Cross-package enums were silently skipped (silent wrong output). Generation now fails loudly via validateEnumFieldEncoding when a custom-enum field references an enum from another Go package, since the marshaler relies on that package's private lookup maps. Full cross-package support remains tracked in #213. Adds a UseProtoNames end-to-end assertion to examples/enum-encoding and updates the consistency test for the two-key patch shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 96cc343 commit 0aad7b8

36 files changed

Lines changed: 3431 additions & 10 deletions

examples/enum-encoding/Makefile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.PHONY: test demo install generate run clean curl
2+
3+
# Default: generate code and run the non-blocking end-to-end proof.
4+
test: generate
5+
@go test ./...
6+
7+
# Demo workflow - generates code, then starts the blocking server (Ctrl+C to stop).
8+
demo: generate run
9+
10+
# Install required tools
11+
install:
12+
@echo "Installing sebuf plugins..."
13+
@go install github.com/bufbuild/buf/cmd/buf@latest
14+
@go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
15+
@GOPROXY=direct go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-go-http@latest
16+
@echo "Tools installed"
17+
18+
# Generate code from proto files (requires ../../bin plugins: run `make build` at repo root)
19+
generate:
20+
@echo "Generating code..."
21+
@buf generate
22+
@go mod tidy
23+
@echo "Code generated"
24+
25+
# Run the server (blocks until Ctrl+C; hit it from another terminal with `make curl`)
26+
run:
27+
@echo "Starting server on :8080 (Ctrl+C to stop). From another terminal: make curl"
28+
@go run main.go
29+
30+
# Hit the running server (start it with `make run` first)
31+
curl:
32+
@curl -s localhost:8080/api/v1/suggestion -d '{"symbol":"AAPL","requestedRisk":"low"}'
33+
@echo ""
34+
35+
# Clean generated files
36+
clean:
37+
@rm -rf api
38+
@echo "Cleaned generated files"

examples/enum-encoding/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# enum-encoding
2+
3+
Demonstrates the `(sebuf.http.enum_value)` annotation end-to-end on the generated
4+
Go HTTP server.
5+
6+
The `RiskLevel` enum maps its values to custom JSON strings:
7+
8+
```protobuf
9+
enum RiskLevel {
10+
RISK_LEVEL_UNSPECIFIED = 0; // no mapping -> proto name
11+
RISK_LEVEL_LOW = 1 [(sebuf.http.enum_value) = "low"];
12+
RISK_LEVEL_MEDIUM = 2 [(sebuf.http.enum_value) = "medium"];
13+
RISK_LEVEL_HIGH = 3 [(sebuf.http.enum_value) = "high"];
14+
}
15+
```
16+
17+
The generated server serializes responses and parses requests through `protojson`,
18+
which by itself emits the raw proto names (`RISK_LEVEL_LOW`). sebuf generates a
19+
message-level marshaler (`*_enum_field_encoding.pb.go`) that rewrites enum fields to
20+
their custom strings on the way out and back on the way in, so the wire format matches
21+
the OpenAPI docs and the TypeScript/Python clients.
22+
23+
## What it proves
24+
25+
`POST /api/v1/suggestion` with body `{"symbol":"AAPL","requestedRisk":"low"}` returns:
26+
27+
```json
28+
{
29+
"symbol": "AAPL",
30+
"probabilityOfProfit": 0.62,
31+
"riskLevel": "low",
32+
"alternateRiskLevels": ["medium", "high"],
33+
"riskBySymbol": {"AAPL": "low", "TSLA": "high"}
34+
}
35+
```
36+
37+
- `"low"` in the request body is accepted (request parsing).
38+
- `riskLevel`, the repeated `alternateRiskLevels`, and the `riskBySymbol` map values
39+
all serialize as custom strings — never `RISK_LEVEL_LOW`.
40+
41+
## Run
42+
43+
```bash
44+
# from the repo root, build the plugins first:
45+
make build
46+
47+
cd examples/enum-encoding
48+
make generate # buf generate + go mod tidy
49+
make test # end-to-end assertion (main_test.go)
50+
make run # start the server on :8080, then: make curl
51+
```

examples/enum-encoding/api/proto/services/suggestion_service.pb.go

Lines changed: 304 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)