Skip to content

Commit 2ebbcde

Browse files
docs: update REQUIREMENTS.md and CLAUDE.md for Phase 7 completion
- Mark JSON-04 (oneof discriminator) and JSON-08 (flatten) as Complete - Fix JSON-04 annotation naming: oneof_config, not oneof_discriminator - Add comprehensive JSON mapping annotations section to CLAUDE.md - Add annotation extension number registry table - Add internal/annotations/ to project structure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d8e1578 commit 2ebbcde

2 files changed

Lines changed: 130 additions & 5 deletions

File tree

.planning/REQUIREMENTS.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ Requirements for v1.0 release. Each must work across all generators (go-http, go
2323
- [x] **JSON-01**: #87 Nullable primitives -- per-field `nullable` annotation; generates pointer types in Go, `| null` union in TS, `nullable: true` in OpenAPI
2424
- [x] **JSON-02**: #88 int64/uint64 as string encoding -- per-field `int64_encoding` annotation with NUMBER/STRING options
2525
- [x] **JSON-03**: #89 Enum string encoding with custom values -- per-enum `enum_encoding` and per-value `enum_value` annotations
26-
- [ ] **JSON-04**: #90 Oneof as discriminated union -- per-oneof `oneof_discriminator` and `oneof_flatten` annotations with field collision detection at generation time
26+
- [x] **JSON-04**: #90 Oneof as discriminated union -- per-oneof `oneof_config` annotation (with `discriminator` and `flatten` fields) and per-field `oneof_value` annotation, with field collision detection at generation time
2727
- [x] **JSON-05**: #92 Multiple timestamp formats -- per-field `timestamp_format` annotation (RFC3339, UNIX_SECONDS, UNIX_MILLIS, DATE)
2828
- [x] **JSON-06**: #93 Empty object handling -- per-field `omit_empty` and `empty_behavior` annotations (PRESERVE, NULL, OMIT)
2929
- [x] **JSON-07**: #95 Bytes encoding options -- per-field `bytes_encoding` annotation (BASE64, BASE64_RAW, BASE64URL, BASE64URL_RAW, HEX)
30-
- [ ] **JSON-08**: #96 Nested message flattening -- per-field `flatten` and `flatten_prefix` annotations with collision detection at generation time
30+
- [x] **JSON-08**: #96 Nested message flattening -- per-field `flatten` and `flatten_prefix` annotations with collision detection at generation time
3131

3232
### Language Clients
3333

@@ -90,11 +90,11 @@ Which phases cover which requirements. Updated during roadmap creation.
9090
| JSON-01 | Phase 5 | Complete |
9191
| JSON-02 | Phase 4 | Complete |
9292
| JSON-03 | Phase 4 | Complete |
93-
| JSON-04 | Phase 7 | Pending |
93+
| JSON-04 | Phase 7 | Complete |
9494
| JSON-05 | Phase 6 | Complete |
9595
| JSON-06 | Phase 5 | Complete |
9696
| JSON-07 | Phase 6 | Complete |
97-
| JSON-08 | Phase 7 | Pending |
97+
| JSON-08 | Phase 7 | Complete |
9898
| LANG-01 | Phase 8 | Pending |
9999
| LANG-02 | Phase 9 | Pending |
100100
| LANG-03 | Phase 10 | Pending |
@@ -112,4 +112,4 @@ Which phases cover which requirements. Updated during roadmap creation.
112112

113113
---
114114
*Requirements defined: 2026-02-05*
115-
*Last updated: 2026-02-06 after Phase 6 completion (JSON-05, JSON-07 marked Complete)*
115+
*Last updated: 2026-02-07 after Phase 7 completion (JSON-04, JSON-08 marked Complete)*

CLAUDE.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,130 @@ message BarsResponse {
220220
// Without unwrap: {"data": {"AAPL": {"bars": [...]}, ...}}
221221
```
222222

223+
**JSON Mapping Annotations** - Control how protobuf fields serialize to JSON across all generators:
224+
225+
**int64_encoding** - Controls int64/uint64 JSON encoding (ext 50010):
226+
```protobuf
227+
message Order {
228+
// Serializes as JSON number: 12345 (precision warning for values > 2^53)
229+
int64 amount = 1 [(sebuf.http.int64_encoding) = INT64_ENCODING_NUMBER];
230+
// Serializes as JSON string: "12345" (default, safe for JavaScript)
231+
uint64 id = 2 [(sebuf.http.int64_encoding) = INT64_ENCODING_STRING];
232+
}
233+
```
234+
235+
**enum_encoding / enum_value** - Controls enum JSON encoding (ext 50011, 50012):
236+
```protobuf
237+
enum Status {
238+
STATUS_UNSPECIFIED = 0;
239+
STATUS_ACTIVE = 1 [(sebuf.http.enum_value) = "active"];
240+
STATUS_INACTIVE = 2 [(sebuf.http.enum_value) = "inactive"];
241+
}
242+
243+
message User {
244+
// With enum_value: serializes as "active" instead of "STATUS_ACTIVE"
245+
Status status = 1 [(sebuf.http.enum_encoding) = ENUM_ENCODING_STRING];
246+
// Serializes as number: 1
247+
Status role = 2 [(sebuf.http.enum_encoding) = ENUM_ENCODING_NUMBER];
248+
}
249+
```
250+
251+
**nullable** - Explicit null semantics for primitive fields (ext 50013):
252+
```protobuf
253+
message Profile {
254+
// Three states: absent (omitted), null, or "value"
255+
// Requires proto3 optional keyword
256+
optional string bio = 1 [(sebuf.http.nullable) = true];
257+
}
258+
// Set: {"bio": "hello"} | Null: {"bio": null} | Absent: {}
259+
```
260+
261+
**empty_behavior** - Controls empty message field serialization (ext 50014):
262+
```protobuf
263+
message Response {
264+
Metadata meta = 1 [(sebuf.http.empty_behavior) = EMPTY_BEHAVIOR_PRESERVE]; // {}
265+
Metadata audit = 2 [(sebuf.http.empty_behavior) = EMPTY_BEHAVIOR_NULL]; // null
266+
Metadata debug = 3 [(sebuf.http.empty_behavior) = EMPTY_BEHAVIOR_OMIT]; // omitted
267+
}
268+
```
269+
270+
**timestamp_format** - Controls google.protobuf.Timestamp serialization (ext 50015):
271+
```protobuf
272+
message Event {
273+
google.protobuf.Timestamp created_at = 1; // Default: "2024-01-15T09:30:00Z"
274+
google.protobuf.Timestamp unix_ts = 2 [(sebuf.http.timestamp_format) = TIMESTAMP_FORMAT_UNIX_SECONDS]; // 1705312200
275+
google.protobuf.Timestamp unix_ms = 3 [(sebuf.http.timestamp_format) = TIMESTAMP_FORMAT_UNIX_MILLIS]; // 1705312200000
276+
google.protobuf.Timestamp date = 4 [(sebuf.http.timestamp_format) = TIMESTAMP_FORMAT_DATE]; // "2024-01-15"
277+
}
278+
```
279+
280+
**bytes_encoding** - Controls bytes field serialization (ext 50016):
281+
```protobuf
282+
message Document {
283+
bytes data = 1; // Default: standard base64 "SGVsbG8="
284+
bytes hash = 2 [(sebuf.http.bytes_encoding) = BYTES_ENCODING_HEX]; // "48656c6c6f"
285+
bytes token = 3 [(sebuf.http.bytes_encoding) = BYTES_ENCODING_BASE64URL]; // URL-safe base64
286+
bytes raw = 4 [(sebuf.http.bytes_encoding) = BYTES_ENCODING_BASE64_RAW]; // No padding
287+
}
288+
```
289+
290+
**oneof_config / oneof_value** - Discriminated unions for oneof fields (ext 50017, 50018):
291+
```protobuf
292+
message Event {
293+
string id = 1;
294+
oneof payload {
295+
option (sebuf.http.oneof_config) = {
296+
discriminator: "type"
297+
flatten: true
298+
};
299+
TextPayload text = 2 [(sebuf.http.oneof_value) = "text"];
300+
ImagePayload image = 3 [(sebuf.http.oneof_value) = "image"];
301+
}
302+
}
303+
// Flattened: {"id": "1", "type": "text", "body": "hello"}
304+
// Not flattened: {"id": "1", "type": "text", "text": {"body": "hello"}}
305+
```
306+
307+
**flatten / flatten_prefix** - Promote nested message fields to parent (ext 50019, 50020):
308+
```protobuf
309+
message Order {
310+
string id = 1;
311+
Address billing = 2 [
312+
(sebuf.http.flatten) = true,
313+
(sebuf.http.flatten_prefix) = "billing_"
314+
];
315+
Address shipping = 3 [
316+
(sebuf.http.flatten) = true,
317+
(sebuf.http.flatten_prefix) = "shipping_"
318+
];
319+
}
320+
// JSON: {"id": "1", "billing_street": "123 Main", "shipping_street": "456 Oak"}
321+
// Without flatten: {"id": "1", "billing": {"street": "123 Main"}, "shipping": {"street": "456 Oak"}}
322+
```
323+
324+
### Annotation Extension Number Registry
325+
326+
All custom annotations live in `proto/sebuf/http/annotations.proto`:
327+
328+
| Ext # | Name | Target | Purpose |
329+
|-------|------|--------|---------|
330+
| 50003 | config | MethodOptions | HTTP path and method |
331+
| 50004 | service_config | ServiceOptions | Service base path |
332+
| 50007 | field_examples | FieldOptions | Example values for docs |
333+
| 50008 | query | FieldOptions | Query parameter config |
334+
| 50009 | unwrap | FieldOptions | Map value / root unwrapping |
335+
| 50010 | int64_encoding | FieldOptions | int64/uint64 JSON encoding |
336+
| 50011 | enum_encoding | FieldOptions | Enum JSON encoding |
337+
| 50012 | enum_value | EnumValueOptions | Custom enum value string |
338+
| 50013 | nullable | FieldOptions | Nullable primitive fields |
339+
| 50014 | empty_behavior | FieldOptions | Empty message handling |
340+
| 50015 | timestamp_format | FieldOptions | Timestamp JSON format |
341+
| 50016 | bytes_encoding | FieldOptions | Bytes JSON encoding |
342+
| 50017 | oneof_config | OneofOptions | Discriminated union config |
343+
| 50018 | oneof_value | FieldOptions | Custom discriminator value |
344+
| 50019 | flatten | FieldOptions | Nested message flattening |
345+
| 50020 | flatten_prefix | FieldOptions | Prefix for flattened fields |
346+
223347
## Development Commands
224348

225349
### Testing
@@ -421,6 +545,7 @@ The repository contains:
421545
- **cmd/protoc-gen-go-client/**: Go HTTP client plugin entry point
422546
- **cmd/protoc-gen-ts-client/**: TypeScript HTTP client plugin entry point
423547
- **cmd/protoc-gen-openapiv3/**: OpenAPI generation plugin entry point
548+
- **internal/annotations/**: Shared annotation parsing used by all 4 generators (unwrap, query params, headers, JSON mapping)
424549
- **internal/httpgen/**: HTTP handler generation logic and tests
425550
- **internal/clientgen/**: Go HTTP client generation logic and tests
426551
- **internal/tsclientgen/**: TypeScript HTTP client generation logic and tests

0 commit comments

Comments
 (0)