Skip to content

Commit c195b73

Browse files
Merge pull request #1538 from microsoft/mk/fix-v2-examples-serialization-bug
Fix: Examples serialization in a response object during v2->v3 upcasting or vice versa
2 parents 1316ee9 + 7882fdd commit c195b73

22 files changed

+478
-41
lines changed

src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.Linq;
66
using System.Security.Cryptography.X509Certificates;
7+
using System.Xml.Linq;
78
using Microsoft.OpenApi.Any;
89
using Microsoft.OpenApi.Extensions;
910
using Microsoft.OpenApi.Models;
@@ -194,7 +195,8 @@ internal static OpenApiRequestBody CreateRequestBody(
194195
k => k,
195196
_ => new OpenApiMediaType
196197
{
197-
Schema = bodyParameter.Schema
198+
Schema = bodyParameter.Schema,
199+
Examples = bodyParameter.Examples
198200
}),
199201
Extensions = bodyParameter.Extensions
200202
};

src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,17 @@ internal static partial class OpenApiV2Deserializer
9595
"schema",
9696
(o, n) => o.Schema = LoadSchema(n)
9797
},
98+
{
99+
"x-examples",
100+
LoadParameterExamplesExtension
101+
},
98102
};
99103

100104
private static readonly PatternFieldMap<OpenApiParameter> _parameterPatternFields =
101105
new()
102106
{
103-
{s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
107+
{s => s.StartsWith("x-") && !s.Equals(OpenApiConstants.ExamplesExtension, StringComparison.OrdinalIgnoreCase),
108+
(o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
104109
};
105110

106111
private static readonly AnyFieldMap<OpenApiParameter> _parameterAnyFields =
@@ -166,6 +171,12 @@ private static void LoadStyle(OpenApiParameter p, string v)
166171
}
167172
}
168173

174+
private static void LoadParameterExamplesExtension(OpenApiParameter parameter, ParseNode node)
175+
{
176+
var examples = LoadExamplesExtension(node);
177+
node.Context.SetTempStorage(TempStorageKeys.Examples, examples, parameter);
178+
}
179+
169180
private static OpenApiSchema GetOrCreateSchema(OpenApiParameter p)
170181
{
171182
if (p.Schema == null)
@@ -250,6 +261,14 @@ public static OpenApiParameter LoadParameter(ParseNode node, bool loadRequestBod
250261
node.Context.SetTempStorage("schema", null);
251262
}
252263

264+
// load examples from storage and add them to the parameter
265+
var examples = node.Context.GetFromTempStorage<Dictionary<string, OpenApiExample>>(TempStorageKeys.Examples, parameter);
266+
if (examples != null)
267+
{
268+
parameter.Examples = examples;
269+
node.Context.SetTempStorage("examples", null);
270+
}
271+
253272
var isBodyOrFormData = (bool)node.Context.GetFromTempStorage<object>(TempStorageKeys.ParameterIsBodyOrFormData);
254273
if (isBodyOrFormData && !loadRequestBody)
255274
{

src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs

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

4+
using System;
45
using System.Collections.Generic;
56
using Microsoft.OpenApi.Extensions;
67
using Microsoft.OpenApi.Models;
@@ -28,6 +29,10 @@ internal static partial class OpenApiV2Deserializer
2829
"examples",
2930
LoadExamples
3031
},
32+
{
33+
"x-examples",
34+
LoadResponseExamplesExtension
35+
},
3136
{
3237
"schema",
3338
(o, n) => n.Context.SetTempStorage(TempStorageKeys.ResponseSchema, LoadSchema(n), o)
@@ -37,7 +42,8 @@ internal static partial class OpenApiV2Deserializer
3742
private static readonly PatternFieldMap<OpenApiResponse> _responsePatternFields =
3843
new()
3944
{
40-
{s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
45+
{s => s.StartsWith("x-") && !s.Equals(OpenApiConstants.ExamplesExtension, StringComparison.OrdinalIgnoreCase),
46+
(o, p, n) => o.AddExtension(p, LoadExtension(p, n))}
4147
};
4248

4349
private static readonly AnyFieldMap<OpenApiMediaType> _mediaTypeAnyFields =
@@ -69,6 +75,8 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P
6975
?? context.DefaultContentType ?? new List<string> { "application/octet-stream" };
7076

7177
var schema = context.GetFromTempStorage<OpenApiSchema>(TempStorageKeys.ResponseSchema, response);
78+
var examples = context.GetFromTempStorage<Dictionary<string, OpenApiExample>>(TempStorageKeys.Examples, response)
79+
?? new Dictionary<string, OpenApiExample>();
7280

7381
foreach (var produce in produces)
7482
{
@@ -84,20 +92,64 @@ private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, P
8492
{
8593
var mediaType = new OpenApiMediaType
8694
{
87-
Schema = schema
95+
Schema = schema,
96+
Examples = examples
8897
};
8998

9099
response.Content.Add(produce, mediaType);
91100
}
92101
}
93102

94103
context.SetTempStorage(TempStorageKeys.ResponseSchema, null, response);
104+
context.SetTempStorage(TempStorageKeys.Examples, null, response);
95105
context.SetTempStorage(TempStorageKeys.ResponseProducesSet, true, response);
96106
}
97107

108+
private static void LoadResponseExamplesExtension(OpenApiResponse response, ParseNode node)
109+
{
110+
var examples = LoadExamplesExtension(node);
111+
node.Context.SetTempStorage(TempStorageKeys.Examples, examples, response);
112+
}
113+
114+
private static Dictionary<string, OpenApiExample> LoadExamplesExtension(ParseNode node)
115+
{
116+
var mapNode = node.CheckMapNode(OpenApiConstants.ExamplesExtension);
117+
var examples = new Dictionary<string, OpenApiExample>();
118+
119+
foreach (var examplesNode in mapNode)
120+
{
121+
// Load the media type node as an OpenApiExample object
122+
var example = new OpenApiExample();
123+
var exampleNode = examplesNode.Value.CheckMapNode(examplesNode.Name);
124+
foreach (var valueNode in exampleNode)
125+
{
126+
switch (valueNode.Name.ToLowerInvariant())
127+
{
128+
case "summary":
129+
example.Summary = valueNode.Value.GetScalarValue();
130+
break;
131+
case "description":
132+
example.Description = valueNode.Value.GetScalarValue();
133+
break;
134+
case "value":
135+
example.Value = OpenApiAnyConverter.GetSpecificOpenApiAny(valueNode.Value.CreateAny());
136+
break;
137+
case "externalValue":
138+
example.ExternalValue = valueNode.Value.GetScalarValue();
139+
break;
140+
}
141+
}
142+
143+
examples.Add(examplesNode.Name, example);
144+
}
145+
146+
return examples;
147+
}
148+
98149
private static void LoadExamples(OpenApiResponse response, ParseNode node)
99150
{
100151
var mapNode = node.CheckMapNode("examples");
152+
101153
foreach (var mediaTypeNode in mapNode)
102154
{
103155
LoadExample(response, mediaTypeNode.Name, mediaTypeNode.Value);
@@ -108,10 +160,7 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars
108160
{
109161
var exampleNode = node.CreateAny();
110162

111-
if (response.Content == null)
112-
{
113-
response.Content = new Dictionary<string, OpenApiMediaType>();
114-
}
163+
response.Content ??= new Dictionary<string, OpenApiMediaType>();
115164

116165
OpenApiMediaType mediaTypeObject;
117166
if (response.Content.TryGetValue(mediaType, out var value))
@@ -141,6 +190,7 @@ public static OpenApiResponse LoadResponse(ParseNode node)
141190
}
142191

143192
var response = new OpenApiResponse();
193+
144194
foreach (var property in mapNode)
145195
{
146196
property.ParseField(response, _responseFixedFields, _responsePatternFields);

src/Microsoft.OpenApi.Readers/V2/TempStorageKeys.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ internal static class TempStorageKeys
1717
public const string GlobalConsumes = "globalConsumes";
1818
public const string GlobalProduces = "globalProduces";
1919
public const string ParameterIsBodyOrFormData = "parameterIsBodyOrFormData";
20+
public const string Examples = "examples";
2021
}
2122
}

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,11 @@ public static class OpenApiConstants
565565
/// </summary>
566566
public const string BodyName = "x-bodyName";
567567

568+
/// <summary>
569+
/// Field: Examples Extension
570+
/// </summary>
571+
public const string ExamplesExtension = "x-examples";
572+
568573
/// <summary>
569574
/// Field: version3_0_0
570575
/// </summary>

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

44
using System;

src/Microsoft.OpenApi/Models/OpenApiExample.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ public OpenApiExample GetEffective(OpenApiDocument doc)
118118
/// Serialize to OpenAPI V3 document without using reference.
119119
/// </summary>
120120
public void SerializeAsV3WithoutReference(IOpenApiWriter writer)
121+
{
122+
Serialize(writer, OpenApiSpecVersion.OpenApi3_0);
123+
}
124+
125+
/// <summary>
126+
/// Writes out existing examples in a mediatype object
127+
/// </summary>
128+
/// <param name="writer"></param>
129+
/// <param name="version"></param>
130+
public void Serialize(IOpenApiWriter writer, OpenApiSpecVersion version)
121131
{
122132
writer.WriteStartObject();
123133

@@ -134,7 +144,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer)
134144
writer.WriteProperty(OpenApiConstants.ExternalValue, ExternalValue);
135145

136146
// extensions
137-
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0);
147+
writer.WriteExtensions(Extensions, version);
138148

139149
writer.WriteEndObject();
140150
}

src/Microsoft.OpenApi/Models/OpenApiParameter.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using Microsoft.OpenApi.Any;
78
using Microsoft.OpenApi.Extensions;
89
using Microsoft.OpenApi.Interfaces;
@@ -204,14 +205,7 @@ public void SerializeAsV3(IOpenApiWriter writer)
204205
/// <returns>OpenApiParameter</returns>
205206
public OpenApiParameter GetEffective(OpenApiDocument doc)
206207
{
207-
if (this.Reference != null)
208-
{
209-
return doc.ResolveReferenceTo<OpenApiParameter>(this.Reference);
210-
}
211-
else
212-
{
213-
return this;
214-
}
208+
return Reference != null ? doc.ResolveReferenceTo<OpenApiParameter>(Reference) : this;
215209
}
216210

217211
/// <summary>
@@ -394,6 +388,20 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer)
394388
}
395389
}
396390

391+
//examples
392+
if (Examples != null && Examples.Any())
393+
{
394+
writer.WritePropertyName(OpenApiConstants.ExamplesExtension);
395+
writer.WriteStartObject();
396+
397+
foreach (var example in Examples)
398+
{
399+
writer.WritePropertyName(example.Key);
400+
example.Value.Serialize(writer, OpenApiSpecVersion.OpenApi2_0);
401+
}
402+
writer.WriteEndObject();
403+
}
404+
397405
// extensions
398406
writer.WriteExtensions(extensionsClone, OpenApiSpecVersion.OpenApi2_0);
399407

src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,7 @@ public void SerializeAsV3(IOpenApiWriter writer)
9696
/// <returns>OpenApiRequestBody</returns>
9797
public OpenApiRequestBody GetEffective(OpenApiDocument doc)
9898
{
99-
if (this.Reference != null)
100-
{
101-
return doc.ResolveReferenceTo<OpenApiRequestBody>(this.Reference);
102-
}
103-
else
104-
{
105-
return this;
106-
}
99+
return Reference != null ? doc.ResolveReferenceTo<OpenApiRequestBody>(Reference) : this;
107100
}
108101

109102
/// <summary>
@@ -153,6 +146,7 @@ internal OpenApiBodyParameter ConvertToBodyParameter()
153146
// To allow round-tripping we use an extension to hold the name
154147
Name = "body",
155148
Schema = Content.Values.FirstOrDefault()?.Schema ?? new OpenApiSchema(),
149+
Examples = Content.Values.FirstOrDefault()?.Examples,
156150
Required = Required,
157151
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.
158152
};
@@ -184,7 +178,8 @@ internal IEnumerable<OpenApiFormDataParameter> ConvertToFormDataParameters()
184178
Description = property.Value.Description,
185179
Name = property.Key,
186180
Schema = property.Value,
187-
Required = Content.First().Value.Schema.Required.Contains(property.Key)
181+
Examples = Content.Values.FirstOrDefault()?.Examples,
182+
Required = Content.First().Value.Schema.Required?.Contains(property.Key) ?? false
188183
};
189184
}
190185
}

src/Microsoft.OpenApi/Models/OpenApiResponse.cs

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

44
using System.Collections.Generic;
@@ -101,14 +101,7 @@ public void SerializeAsV3(IOpenApiWriter writer)
101101
/// <returns>OpenApiResponse</returns>
102102
public OpenApiResponse GetEffective(OpenApiDocument doc)
103103
{
104-
if (this.Reference != null)
105-
{
106-
return doc.ResolveReferenceTo<OpenApiResponse>(this.Reference);
107-
}
108-
else
109-
{
110-
return this;
111-
}
104+
return Reference != null ? doc.ResolveReferenceTo<OpenApiResponse>(Reference) : this;
112105
}
113106

114107
/// <summary>
@@ -201,6 +194,22 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer)
201194
writer.WriteEndObject();
202195
}
203196

197+
if (Content.Values.Any(m => m.Examples != null && m.Examples.Any()))
198+
{
199+
writer.WritePropertyName(OpenApiConstants.ExamplesExtension);
200+
writer.WriteStartObject();
201+
202+
foreach (var example in Content
203+
.Where(mediaTypePair => mediaTypePair.Value.Examples != null && mediaTypePair.Value.Examples.Any())
204+
.SelectMany(mediaTypePair => mediaTypePair.Value.Examples))
205+
{
206+
writer.WritePropertyName(example.Key);
207+
example.Value.Serialize(writer, OpenApiSpecVersion.OpenApi2_0);
208+
}
209+
210+
writer.WriteEndObject();
211+
}
212+
204213
writer.WriteExtensions(mediatype.Value.Extensions, OpenApiSpecVersion.OpenApi2_0);
205214

206215
foreach (var key in mediatype.Value.Extensions.Keys)

0 commit comments

Comments
 (0)