Skip to content

Commit 76321be

Browse files
Fix $ref serialization errors for requestBodies and responses (#1033)
* Do not add empty consumes array if RequestBody content contains no elements * Add requestbody references as parameter references in v2 serialization * Add consumes and produces properties when we have reference request bodies and responses respectively during v2 serialization * Update src/Microsoft.OpenApi/Models/OpenApiOperation.cs Co-authored-by: Vincent Biret <[email protected]> * Apply suggestions from code review Co-authored-by: Vincent Biret <[email protected]> * Address code review comments Co-authored-by: Vincent Biret <[email protected]>
1 parent 680ee90 commit 76321be

File tree

4 files changed

+108
-76
lines changed

4 files changed

+108
-76
lines changed

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public void SerializeAsV2(IOpenApiWriter writer)
160160
// paths
161161
writer.WriteRequiredObject(OpenApiConstants.Paths, Paths, (w, p) => p.SerializeAsV2(w));
162162

163-
// If references have been inlined we don't need the to render the components section
163+
// If references have been inlined we don't need to render the components section
164164
// however if they have cycles, then we will need a component rendered
165165
if (writer.GetSettings().InlineLocalReferences)
166166
{
@@ -208,9 +208,20 @@ public void SerializeAsV2(IOpenApiWriter writer)
208208
});
209209
}
210210
// parameters
211+
var parameters = Components?.Parameters != null
212+
? new Dictionary<string, OpenApiParameter>(Components.Parameters)
213+
: new Dictionary<string, OpenApiParameter>();
214+
215+
if (Components?.RequestBodies != null)
216+
{
217+
foreach (var requestBody in Components.RequestBodies.Where(b => !parameters.ContainsKey(b.Key)))
218+
{
219+
parameters.Add(requestBody.Key, requestBody.Value.ConvertToBodyParameter());
220+
}
221+
}
211222
writer.WriteOptionalMap(
212223
OpenApiConstants.Parameters,
213-
Components?.Parameters,
224+
parameters,
214225
(w, key, component) =>
215226
{
216227
if (component.Reference != null &&

src/Microsoft.OpenApi/Models/OpenApiOperation.cs

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ public void SerializeAsV2(IOpenApiWriter writer)
224224
// operationId
225225
writer.WriteProperty(OpenApiConstants.OperationId, OperationId);
226226

227-
IList<OpenApiParameter> parameters;
227+
List<OpenApiParameter> parameters;
228228
if (Parameters == null)
229229
{
230230
parameters = new List<OpenApiParameter>();
@@ -237,70 +237,58 @@ public void SerializeAsV2(IOpenApiWriter writer)
237237
if (RequestBody != null)
238238
{
239239
// consumes
240-
writer.WritePropertyName(OpenApiConstants.Consumes);
241-
writer.WriteStartArray();
242240
var consumes = RequestBody.Content.Keys.Distinct().ToList();
243-
foreach (var mediaType in consumes)
241+
if (consumes.Any())
244242
{
245-
writer.WriteValue(mediaType);
246-
}
247-
248-
writer.WriteEndArray();
249-
250-
// This is form data. We need to split the request body into multiple parameters.
251-
if (consumes.Contains("application/x-www-form-urlencoded") ||
252-
consumes.Contains("multipart/form-data"))
253-
{
254-
foreach (var property in RequestBody.Content.First().Value.Schema.Properties)
243+
// This is form data. We need to split the request body into multiple parameters.
244+
if (consumes.Contains("application/x-www-form-urlencoded") ||
245+
consumes.Contains("multipart/form-data"))
255246
{
256-
var paramSchema = property.Value;
257-
if ("string".Equals(paramSchema.Type, StringComparison.OrdinalIgnoreCase)
258-
&& ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)
259-
|| "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)))
260-
{
261-
paramSchema.Type = "file";
262-
paramSchema.Format = null;
263-
}
264-
parameters.Add(
265-
new OpenApiFormDataParameter
266-
{
267-
Description = property.Value.Description,
268-
Name = property.Key,
269-
Schema = property.Value,
270-
Required = RequestBody.Content.First().Value.Schema.Required.Contains(property.Key)
271-
272-
});
247+
parameters.AddRange(RequestBody.ConvertToFormDataParameters());
248+
}
249+
else
250+
{
251+
parameters.Add(RequestBody.ConvertToBodyParameter());
273252
}
274253
}
275-
else
254+
else if (RequestBody.Reference != null)
276255
{
277-
var content = RequestBody.Content.Values.FirstOrDefault();
256+
parameters.Add(
257+
new OpenApiParameter
258+
{
259+
UnresolvedReference = true,
260+
Reference = RequestBody.Reference
261+
});
278262

279-
var bodyParameter = new OpenApiBodyParameter
263+
if (RequestBody.Reference.HostDocument != null)
280264
{
281-
Description = RequestBody.Description,
282-
// V2 spec actually allows the body to have custom name.
283-
// To allow round-tripping we use an extension to hold the name
284-
Name = "body",
285-
Schema = content?.Schema ?? new OpenApiSchema(),
286-
Required = RequestBody.Required,
287-
Extensions = RequestBody.Extensions.ToDictionary(k => k.Key, v => v.Value) // Clone extensions so we can remove the x-bodyName extensions from the output V2 model.
288-
};
289-
290-
if (bodyParameter.Extensions.ContainsKey(OpenApiConstants.BodyName))
265+
var effectiveRequestBody = RequestBody.GetEffective(RequestBody.Reference.HostDocument);
266+
if (effectiveRequestBody != null)
267+
consumes = effectiveRequestBody.Content.Keys.Distinct().ToList();
268+
}
269+
}
270+
271+
if (consumes.Any())
272+
{
273+
writer.WritePropertyName(OpenApiConstants.Consumes);
274+
writer.WriteStartArray();
275+
foreach (var mediaType in consumes)
291276
{
292-
bodyParameter.Name = (RequestBody.Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body";
293-
bodyParameter.Extensions.Remove(OpenApiConstants.BodyName);
277+
writer.WriteValue(mediaType);
294278
}
295-
296-
parameters.Add(bodyParameter);
279+
writer.WriteEndArray();
297280
}
298281
}
299282

300283
if (Responses != null)
301284
{
302-
var produces = Responses.Where(r => r.Value.Content != null)
303-
.SelectMany(r => r.Value.Content?.Keys)
285+
var produces = Responses
286+
.Where(static r => r.Value.Content != null)
287+
.SelectMany(static r => r.Value.Content?.Keys)
288+
.Concat(
289+
Responses
290+
.Where(static r => r.Value.Reference != null && r.Value.Reference.HostDocument != null)
291+
.SelectMany(static r => r.Value.GetEffective(r.Value.Reference.HostDocument)?.Content?.Keys))
304292
.Distinct()
305293
.ToList();
306294

src/Microsoft.OpenApi/Models/OpenApiReference.cs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -214,31 +214,17 @@ private string GetExternalReferenceV2()
214214

215215
private string GetReferenceTypeNameAsV2(ReferenceType type)
216216
{
217-
switch (type)
217+
return type switch
218218
{
219-
case ReferenceType.Schema:
220-
return OpenApiConstants.Definitions;
221-
222-
case ReferenceType.Parameter:
223-
return OpenApiConstants.Parameters;
224-
225-
case ReferenceType.Response:
226-
return OpenApiConstants.Responses;
227-
228-
case ReferenceType.Header:
229-
return OpenApiConstants.Headers;
230-
231-
case ReferenceType.Tag:
232-
return OpenApiConstants.Tags;
233-
234-
case ReferenceType.SecurityScheme:
235-
return OpenApiConstants.SecurityDefinitions;
236-
237-
default:
238-
// If the reference type is not supported in V2, simply return null
239-
// to indicate that the reference is not pointing to any object.
240-
return null;
241-
}
219+
ReferenceType.Schema => OpenApiConstants.Definitions,
220+
ReferenceType.Parameter or ReferenceType.RequestBody => OpenApiConstants.Parameters,
221+
ReferenceType.Response => OpenApiConstants.Responses,
222+
ReferenceType.Header => OpenApiConstants.Headers,
223+
ReferenceType.Tag => OpenApiConstants.Tags,
224+
ReferenceType.SecurityScheme => OpenApiConstants.SecurityDefinitions,
225+
_ => null,// If the reference type is not supported in V2, simply return null
226+
// to indicate that the reference is not pointing to any object.
227+
};
242228
}
243229
}
244230
}

src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
6+
using System.Linq;
57
using Microsoft.OpenApi.Any;
68
using Microsoft.OpenApi.Interfaces;
79
using Microsoft.OpenApi.Writers;
@@ -144,5 +146,50 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer)
144146
{
145147
// RequestBody object does not exist in V2.
146148
}
149+
150+
internal OpenApiBodyParameter ConvertToBodyParameter()
151+
{
152+
var bodyParameter = new OpenApiBodyParameter
153+
{
154+
Description = Description,
155+
// V2 spec actually allows the body to have custom name.
156+
// To allow round-tripping we use an extension to hold the name
157+
Name = "body",
158+
Schema = Content.Values.FirstOrDefault()?.Schema ?? new OpenApiSchema(),
159+
Required = Required,
160+
Extensions = Extensions.ToDictionary(static k => k.Key, static v => v.Value) // Clone extensions so we can remove the x-bodyName extensions from the output V2 model.
161+
};
162+
if (bodyParameter.Extensions.ContainsKey(OpenApiConstants.BodyName))
163+
{
164+
bodyParameter.Name = (Extensions[OpenApiConstants.BodyName] as OpenApiString)?.Value ?? "body";
165+
bodyParameter.Extensions.Remove(OpenApiConstants.BodyName);
166+
}
167+
return bodyParameter;
168+
}
169+
170+
internal IEnumerable<OpenApiFormDataParameter> ConvertToFormDataParameters()
171+
{
172+
if (Content == null || !Content.Any())
173+
yield break;
174+
175+
foreach (var property in Content.First().Value.Schema.Properties)
176+
{
177+
var paramSchema = property.Value;
178+
if ("string".Equals(paramSchema.Type, StringComparison.OrdinalIgnoreCase)
179+
&& ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)
180+
|| "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)))
181+
{
182+
paramSchema.Type = "file";
183+
paramSchema.Format = null;
184+
}
185+
yield return new OpenApiFormDataParameter
186+
{
187+
Description = property.Value.Description,
188+
Name = property.Key,
189+
Schema = property.Value,
190+
Required = Content.First().Value.Schema.Required.Contains(property.Key)
191+
};
192+
}
193+
}
147194
}
148195
}

0 commit comments

Comments
 (0)