Skip to content

Commit aac008e

Browse files
vitorbariclaude
andauthored
fix: generate requestBody for messages with oneOf and path params (#234)
When a message has both regular fields and oneOf fields, MessageToSchema wraps them under AllOf and sets Properties to nil. The body:"*" code path only checked s.Properties, so it skipped generating the requestBody entirely for these messages. Look for properties inside the AllOf entries when s.Properties is nil, and check for AllOf/OneOf content when deciding whether to emit a requestBody. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 30c93de commit aac008e

File tree

6 files changed

+468
-12
lines changed

6 files changed

+468
-12
lines changed

internal/converter/converter_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var scenarios = []Scenario{
3434
{Name: "with_base", Options: "base=testdata/with_base/base.yaml,trim-unused-types"},
3535
{Name: "with_specification_extensions", Options: "base=testdata/with_specification_extensions/base.yaml,trim-unused-types"},
3636
{Name: "additional_bindings"},
37+
{Name: "path_params"},
3738
{Name: "with_override", Options: "override=testdata/with_override/override.yaml"},
3839
{Name: "with_service_filters", Options: "services=**.User*"},
3940
{Name: "with_google_error_detail", Options: "with-google-error-detail"},

internal/converter/googleapi/paths.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -240,21 +240,35 @@ func httpRuleToPathMap(opts options.Options, md protoreflect.MethodDescriptor, r
240240
case "*":
241241
if len(fieldNamesInPath) > 0 {
242242
_, s := schema.MessageToSchema(opts, md.Input())
243-
if s != nil && s.Properties != nil {
244-
for name := range fieldNamesInPath {
245-
s.Properties.Delete(name)
246-
// Also remove from required list to prevent duplicate required properties
247-
if s.Required != nil {
248-
s.Required = slices.DeleteFunc(s.Required, func(s string) bool {
249-
return s == name
250-
})
251-
// don't serialize []
252-
if len(s.Required) == 0 {
253-
s.Required = nil
243+
if s != nil {
244+
// Remove path parameters from properties.
245+
// When the message has oneOf fields, MessageToSchema wraps
246+
// properties and oneOf under AllOf, setting s.Properties to nil.
247+
// In that case, find the properties inside AllOf.
248+
props := s.Properties
249+
if props == nil && len(s.AllOf) > 0 {
250+
for _, entry := range s.AllOf {
251+
if es := entry.Schema(); es != nil && es.Properties != nil && es.Properties.Len() > 0 {
252+
props = es.Properties
253+
break
254254
}
255255
}
256256
}
257-
if s.Properties.Len() > 0 {
257+
if props != nil {
258+
for name := range fieldNamesInPath {
259+
props.Delete(name)
260+
if s.Required != nil {
261+
s.Required = slices.DeleteFunc(s.Required, func(s string) bool {
262+
return s == name
263+
})
264+
if len(s.Required) == 0 {
265+
s.Required = nil
266+
}
267+
}
268+
}
269+
}
270+
hasContent := (props != nil && props.Len() > 0) || len(s.AllOf) > 0 || len(s.OneOf) > 0
271+
if hasContent {
258272
op.RequestBody = util.MethodToRequestBody(opts, md, base.CreateSchemaProxy(s), false)
259273
}
260274
}
1.15 KB
Binary file not shown.
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": "path_params"
5+
},
6+
"paths": {
7+
"/go/{snake_case}": {
8+
"post": {
9+
"tags": [
10+
"path_params.PathParams"
11+
],
12+
"summary": "Go",
13+
"operationId": "path_params.PathParams.Go",
14+
"parameters": [
15+
{
16+
"name": "snake_case",
17+
"in": "path",
18+
"required": true,
19+
"schema": {
20+
"type": "string",
21+
"title": "snake_case"
22+
}
23+
}
24+
],
25+
"requestBody": {
26+
"content": {
27+
"application/json": {
28+
"schema": {
29+
"type": "object",
30+
"properties": {
31+
"somethingElse": {
32+
"type": "string",
33+
"title": "something_else"
34+
}
35+
},
36+
"title": "Request",
37+
"additionalProperties": false
38+
}
39+
}
40+
},
41+
"required": true
42+
},
43+
"responses": {
44+
"200": {
45+
"description": "Success",
46+
"content": {
47+
"application/json": {
48+
"schema": {
49+
"$ref": "#/components/schemas/google.protobuf.Empty"
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
},
57+
"/resources/{id}": {
58+
"patch": {
59+
"tags": [
60+
"path_params.PathParams"
61+
],
62+
"summary": "Update",
63+
"description": "Update has a path param and a oneOf in the request body.",
64+
"operationId": "path_params.PathParams.Update",
65+
"parameters": [
66+
{
67+
"name": "id",
68+
"in": "path",
69+
"required": true,
70+
"schema": {
71+
"type": "string",
72+
"title": "id"
73+
}
74+
}
75+
],
76+
"requestBody": {
77+
"content": {
78+
"application/json": {
79+
"schema": {
80+
"type": "object",
81+
"allOf": [
82+
{
83+
"properties": {
84+
"name": {
85+
"type": "string",
86+
"title": "name"
87+
}
88+
}
89+
},
90+
{
91+
"oneOf": [
92+
{
93+
"properties": {
94+
"profile": {
95+
"title": "profile",
96+
"$ref": "#/components/schemas/path_params.Profile"
97+
}
98+
},
99+
"title": "profile",
100+
"required": [
101+
"profile"
102+
]
103+
},
104+
{
105+
"properties": {
106+
"settings": {
107+
"title": "settings",
108+
"$ref": "#/components/schemas/path_params.Settings"
109+
}
110+
},
111+
"title": "settings",
112+
"required": [
113+
"settings"
114+
]
115+
}
116+
]
117+
}
118+
],
119+
"title": "UpdateRequest",
120+
"additionalProperties": false,
121+
"description": "UpdateRequest has a path param, regular fields, and a oneOf.\n This tests that the requestBody is generated correctly when\n MessageToSchema wraps properties+oneOf under AllOf."
122+
}
123+
}
124+
},
125+
"required": true
126+
},
127+
"responses": {
128+
"200": {
129+
"description": "Success",
130+
"content": {
131+
"application/json": {
132+
"schema": {
133+
"$ref": "#/components/schemas/google.protobuf.Empty"
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}
140+
}
141+
},
142+
"components": {
143+
"schemas": {
144+
"google.protobuf.Empty": {
145+
"type": "object",
146+
"description": "A generic empty message that you can re-use to avoid defining duplicated\n empty messages in your APIs. A typical example is to use it as the request\n or the response type of an API method. For instance:\n\n service Foo {\n rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);\n }"
147+
},
148+
"path_params.Profile": {
149+
"type": "object",
150+
"properties": {
151+
"displayName": {
152+
"type": "string",
153+
"title": "display_name"
154+
}
155+
},
156+
"title": "Profile",
157+
"additionalProperties": false
158+
},
159+
"path_params.Request": {
160+
"type": "object",
161+
"properties": {
162+
"snakeCase": {
163+
"type": "string",
164+
"title": "snake_case"
165+
},
166+
"somethingElse": {
167+
"type": "string",
168+
"title": "something_else"
169+
}
170+
},
171+
"title": "Request",
172+
"required": [
173+
"snakeCase"
174+
],
175+
"additionalProperties": false
176+
},
177+
"path_params.Settings": {
178+
"type": "object",
179+
"properties": {
180+
"theme": {
181+
"type": "string",
182+
"title": "theme"
183+
}
184+
},
185+
"title": "Settings",
186+
"additionalProperties": false
187+
},
188+
"path_params.UpdateRequest": {
189+
"type": "object",
190+
"allOf": [
191+
{
192+
"properties": {
193+
"id": {
194+
"type": "string",
195+
"title": "id"
196+
},
197+
"name": {
198+
"type": "string",
199+
"title": "name"
200+
}
201+
}
202+
},
203+
{
204+
"oneOf": [
205+
{
206+
"properties": {
207+
"profile": {
208+
"title": "profile",
209+
"$ref": "#/components/schemas/path_params.Profile"
210+
}
211+
},
212+
"title": "profile",
213+
"required": [
214+
"profile"
215+
]
216+
},
217+
{
218+
"properties": {
219+
"settings": {
220+
"title": "settings",
221+
"$ref": "#/components/schemas/path_params.Settings"
222+
}
223+
},
224+
"title": "settings",
225+
"required": [
226+
"settings"
227+
]
228+
}
229+
]
230+
}
231+
],
232+
"title": "UpdateRequest",
233+
"required": [
234+
"id"
235+
],
236+
"additionalProperties": false,
237+
"description": "UpdateRequest has a path param, regular fields, and a oneOf.\n This tests that the requestBody is generated correctly when\n MessageToSchema wraps properties+oneOf under AllOf."
238+
}
239+
}
240+
},
241+
"security": [],
242+
"tags": [
243+
{
244+
"name": "path_params.PathParams"
245+
}
246+
]
247+
}

0 commit comments

Comments
 (0)