Skip to content

Commit ff1dbc9

Browse files
authored
Merge pull request #2970 from microsoft/fix/examples-serialization
fix: serialize examples as extension in v2/v3
2 parents d090989 + 990719b commit ff1dbc9

14 files changed

Lines changed: 308 additions & 7 deletions

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,11 @@ public static class OpenApiConstants
775775
/// </summary>
776776
public const string OAuth2MetadataUrl = "oauth2MetadataUrl";
777777

778+
/// <summary>
779+
/// Extension: x-oai-oauth2-metadata-url
780+
/// </summary>
781+
internal const string OAuth2MetadataUrlExtension = "x-oai-oauth2-metadata-url";
782+
778783
/// <summary>
779784
/// Field: OpenIdConnectUrl
780785
/// </summary>
@@ -810,6 +815,11 @@ public static class OpenApiConstants
810815
/// </summary>
811816
public const string ExamplesExtension = "x-examples";
812817

818+
/// <summary>
819+
/// Extension: x-jsonschema-examples
820+
/// </summary>
821+
public const string JsonSchemaExamplesExtension = "x-jsonschema-examples";
822+
813823
/// <summary>
814824
/// Field: version3_0_0
815825
/// </summary>

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,10 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
620620
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, callback);
621621

622622
// example
623-
writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
623+
writer.WriteOptionalObject(
624+
OpenApiConstants.Example,
625+
version < OpenApiSpecVersion.OpenApi3_1 ? GetCompatibilityExample() : Example,
626+
(w, e) => w.WriteAny(e));
624627

625628
// deprecated
626629
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);
@@ -746,6 +749,7 @@ private void WriteV3CompatibilityKeywords(IOpenApiWriter writer, Action<IOpenApi
746749
writer.WriteOptionalObject(OpenApiConstants.IfExtension, If, callback);
747750
writer.WriteOptionalObject(OpenApiConstants.ThenExtension, Then, callback);
748751
writer.WriteOptionalObject(OpenApiConstants.ElseExtension, Else, callback);
752+
writer.WriteOptionalCollection(OpenApiConstants.JsonSchemaExamplesExtension, GetCompatibilityExamplesExtension(), (nodeWriter, s) => nodeWriter.WriteAny(s));
749753
}
750754

751755
internal void WriteAsItemsProperties(IOpenApiWriter writer)
@@ -959,7 +963,7 @@ private void SerializeAsV2(
959963
writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, s) => s.SerializeAsV2(w));
960964

961965
// example
962-
writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e));
966+
writer.WriteOptionalObject(OpenApiConstants.Example, GetCompatibilityExample(), (w, e) => w.WriteAny(e));
963967

964968
// x-nullable extension
965969
SerializeNullable(writer, OpenApiSpecVersion.OpenApi2_0);
@@ -990,6 +994,8 @@ private void SerializeAsV2(
990994
writer.WriteOptionalMap(OpenApiConstants.PatternPropertiesExtension, PatternProperties, (w, s) => s.SerializeAsV2(w));
991995
}
992996

997+
writer.WriteOptionalCollection(OpenApiConstants.JsonSchemaExamplesExtension, GetCompatibilityExamplesExtension(), (nodeWriter, s) => nodeWriter.WriteAny(s));
998+
993999
// extensions
9941000
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0);
9951001

@@ -1024,6 +1030,26 @@ private bool TrySerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion
10241030
return false;
10251031
}
10261032

1033+
private JsonNode? GetCompatibilityExample()
1034+
{
1035+
return Example ?? Examples?.FirstOrDefault();
1036+
}
1037+
1038+
private IEnumerable<JsonNode>? GetCompatibilityExamplesExtension()
1039+
{
1040+
if (Examples is null || Examples.Count == 0)
1041+
{
1042+
return null;
1043+
}
1044+
1045+
if (Example is not null)
1046+
{
1047+
return Examples;
1048+
}
1049+
1050+
return Examples.Count > 1 ? Examples.Skip(1) : null;
1051+
}
1052+
10271053
private static bool IsPowerOfTwo(int x)
10281054
{
10291055
return x != 0 && (x & (x - 1)) == 0;

src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
133133
}
134134
else
135135
{
136-
writer.WriteProperty("x-oauth2-metadata-url", OAuth2MetadataUrl?.ToString());
136+
writer.WriteProperty(OpenApiConstants.OAuth2MetadataUrlExtension, OAuth2MetadataUrl?.ToString());
137137
}
138138
writer.WriteOptionalObject(OpenApiConstants.Flows, Flows, callback);
139139
break;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
#nullable enable
2+
const Microsoft.OpenApi.OpenApiConstants.JsonSchemaExamplesExtension = "x-jsonschema-examples" -> string!
23
const Microsoft.OpenApi.OpenApiConstants.OaiLicenseIdentifier = "x-oai-license-identifier" -> string!

src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@ internal static partial class OpenApiV2Deserializer
267267
"example",
268268
(o, n, _, _) => o.Example = n
269269
},
270+
{
271+
OpenApiConstants.JsonSchemaExamplesExtension,
272+
(o, n, _, c) => o.Examples = n.CreateListOfAny(c)
273+
},
270274
{
271275
OpenApiConstants.PatternPropertiesExtension,
272276
(o, n, t, c) => o.PatternProperties = n.CreateMap(LoadSchema, t, c)

src/Microsoft.OpenApi/Reader/V2/OpenApiSecuritySchemeDeserializer.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,21 @@ internal static partial class OpenApiV2Deserializer
9999
private static readonly PatternFieldMap<OpenApiSecurityScheme> _securitySchemePatternFields =
100100
new()
101101
{
102-
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _, c) => o.AddExtension(p, LoadExtension(p, n, c))}
102+
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _, c) =>
103+
{
104+
if (p.Equals(OpenApiConstants.OAuth2MetadataUrlExtension, StringComparison.OrdinalIgnoreCase))
105+
{
106+
var metadataUrl = n.GetScalarValue();
107+
if (metadataUrl != null)
108+
{
109+
o.OAuth2MetadataUrl = new(metadataUrl, UriKind.RelativeOrAbsolute);
110+
}
111+
}
112+
else
113+
{
114+
o.AddExtension(p, LoadExtension(p, n, c));
115+
}
116+
}}
103117
};
104118

105119
public static IOpenApiSecurityScheme LoadSecurityScheme(JsonNode node, OpenApiDocument hostDocument, ParsingContext context)

src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ internal static partial class OpenApiV3Deserializer
280280
"example",
281281
(o, n, _, _) => o.Example = n
282282
},
283+
{
284+
OpenApiConstants.JsonSchemaExamplesExtension,
285+
(o, n, _, c) => o.Examples = n.CreateListOfAny(c)
286+
},
283287
{
284288
"deprecated",
285289
(o, n, _, _) =>

src/Microsoft.OpenApi/Reader/V3/OpenApiSecuritySchemeDeserializer.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,15 @@ internal static partial class OpenApiV3Deserializer
7676
{
7777
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _, c) =>
7878
{
79-
if (p.Equals("x-oai-deprecated", StringComparison.OrdinalIgnoreCase))
79+
if (p.Equals(OpenApiConstants.OAuth2MetadataUrlExtension, StringComparison.OrdinalIgnoreCase))
80+
{
81+
var metadataUrl = n.GetScalarValue();
82+
if (metadataUrl != null)
83+
{
84+
o.OAuth2MetadataUrl = new(metadataUrl, UriKind.RelativeOrAbsolute);
85+
}
86+
}
87+
else if (p.Equals("x-oai-deprecated", StringComparison.OrdinalIgnoreCase))
8088
{
8189
var deprecated = n.GetScalarValue();
8290
if (deprecated != null)

src/Microsoft.OpenApi/Reader/V31/OpenApiSecuritySchemeDeserializer.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,15 @@ internal static partial class OpenApiV31Deserializer
8383
{
8484
{s => s.StartsWith(OpenApiConstants.ExtensionFieldNamePrefix, StringComparison.OrdinalIgnoreCase), (o, p, n, _, c) =>
8585
{
86-
if (p.Equals("x-oai-deprecated", StringComparison.OrdinalIgnoreCase))
86+
if (p.Equals(OpenApiConstants.OAuth2MetadataUrlExtension, StringComparison.OrdinalIgnoreCase))
87+
{
88+
var metadataUrl = n.GetScalarValue();
89+
if (metadataUrl != null)
90+
{
91+
o.OAuth2MetadataUrl = new(metadataUrl, UriKind.RelativeOrAbsolute);
92+
}
93+
}
94+
else if (p.Equals("x-oai-deprecated", StringComparison.OrdinalIgnoreCase))
8795
{
8896
var deprecated = n.GetScalarValue();
8997
if (deprecated != null)

test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSecuritySchemeTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.IO;
7+
using System.Text.Json.Nodes;
78
using Microsoft.OpenApi.Reader;
89
using Microsoft.OpenApi.Reader.V2;
910
using Microsoft.OpenApi.YamlReader;
@@ -209,6 +210,29 @@ public void ParseOAuth2AccessCodeSecuritySchemeShouldSucceed()
209210
}, securityScheme);
210211
}
211212

213+
[Fact]
214+
public void ParseOAuth2SecuritySchemeWithMetadataUrlExtensionShouldSucceed()
215+
{
216+
var json =
217+
"""
218+
{
219+
"type": "oauth2",
220+
"flow": "application",
221+
"tokenUrl": "https://idp.example.com/oauth/token",
222+
"scopes": {
223+
"scope:one": "Scope one"
224+
},
225+
"x-oai-oauth2-metadata-url": "https://idp.example.com/.well-known/oauth-authorization-server"
226+
}
227+
""";
228+
229+
var securityScheme = Assert.IsType<OpenApiSecurityScheme>(
230+
OpenApiV2Deserializer.LoadSecurityScheme(JsonNode.Parse(json)!, new(), new ParsingContext(new())));
231+
232+
Assert.Equal(new Uri("https://idp.example.com/.well-known/oauth-authorization-server"), securityScheme.OAuth2MetadataUrl);
233+
Assert.True(securityScheme.Extensions is null || !securityScheme.Extensions.ContainsKey(OpenApiConstants.OAuth2MetadataUrlExtension));
234+
}
235+
212236
static YamlDocument LoadYamlDocument(Stream input)
213237
{
214238
using var reader = new StreamReader(input);

0 commit comments

Comments
 (0)