diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index b446ccbd4..561e966b5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -214,8 +214,8 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false); // security - writer.WriteOptionalCollection(OpenApiConstants.Security, Security, callback); - + writer.WriteOptionalOrEmptyCollection(OpenApiConstants.Security, Security, callback); + // servers writer.WriteOptionalCollection(OpenApiConstants.Servers, Servers, callback); diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs index ebea2bc40..c0358cfd6 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiOperationDeserializer.cs @@ -9,6 +9,7 @@ using Microsoft.OpenApi.Models.References; using Microsoft.OpenApi.Models.Interfaces; using System; +using System.Text.Json.Nodes; namespace Microsoft.OpenApi.Reader.V2 { @@ -94,7 +95,10 @@ internal static partial class OpenApiV2Deserializer }, { "security", - (o, n, t) => o.Security = n.CreateList(LoadSecurityRequirement, t) + (o, n, t) => { if (n.JsonNode is JsonArray) + { + o.Security = n.CreateList(LoadSecurityRequirement, t); + } } }, }; diff --git a/src/Microsoft.OpenApi/Reader/V3/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi/Reader/V3/OpenApiOperationDeserializer.cs index eb971da7c..8a0e19336 100644 --- a/src/Microsoft.OpenApi/Reader/V3/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V3/OpenApiOperationDeserializer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models.References; @@ -83,7 +84,13 @@ internal static partial class OpenApiV3Deserializer }, { "security", - (o, n, t) => o.Security = n.CreateList(LoadSecurityRequirement, t) + (o, n, t) => + { + if (n.JsonNode is JsonArray) + { + o.Security = n.CreateList(LoadSecurityRequirement, t); + } + } }, { "servers", diff --git a/src/Microsoft.OpenApi/Reader/V31/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi/Reader/V31/OpenApiOperationDeserializer.cs index abf35545b..d04e91750 100644 --- a/src/Microsoft.OpenApi/Reader/V31/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V31/OpenApiOperationDeserializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models.References; @@ -96,8 +97,11 @@ internal static partial class OpenApiV31Deserializer }, { "security", (o, n, t) => - { - o.Security = n.CreateList(LoadSecurityRequirement, t); + { + if (n.JsonNode is JsonArray) + { + o.Security = n.CreateList(LoadSecurityRequirement, t); + } } }, { diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs index 31f60d0fd..c799171c5 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs @@ -217,6 +217,26 @@ public static void WriteOptionalCollection( writer.WriteCollectionInternal(name, elements, action); } } + + /// + /// Write the optional or empty Open API object/element collection. + /// + /// The Open API element type. + /// The Open API writer. + /// The property name. + /// The collection values. + /// The collection element writer action. + public static void WriteOptionalOrEmptyCollection( + this IOpenApiWriter writer, + string name, + IEnumerable? elements, + Action action) + { + if (elements != null) + { + writer.WriteCollectionInternal(name, elements, action); + } + } /// /// Write the required Open API object/element collection. diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 69ebd98a4..bbf1b8742 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -56,5 +56,13 @@ + + + PreserveNewest + + + + PreserveNewest + \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index bbeeaf10f..993de7a2d 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -12,7 +12,6 @@ using Microsoft.OpenApi.Models.Interfaces; using Microsoft.OpenApi.Models.References; using Microsoft.OpenApi.Writers; -using Microsoft.VisualBasic; using VerifyXunit; using Xunit; @@ -2177,7 +2176,7 @@ public void SerializeAsThrowsIfVersionIsNotSupported() } [Fact] - public async Task SerializeDocWithSecuritySchemeWithInlineRefererencesWorks() + public async Task SerializeDocWithSecuritySchemeWithInlineReferencesWorks() { var expected = @"openapi: 3.0.4 info: @@ -2218,5 +2217,75 @@ public async Task SerializeDocWithSecuritySchemeWithInlineRefererencesWorks() var actual = stringWriter.ToString(); Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral()); } + + [Fact] + public async Task SerializeDocWithoutOperationSecurityWorks() + { + var expected = """ + openapi: 3.0.4 + info: + title: Repair Service + version: 1.0.0 + servers: + - url: https://pluginrentu.azurewebsites.net/api + paths: + /repairs: + get: + summary: List all repairs + description: Returns a list of repairs with their details and images + operationId: listRepairs + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: object + """; + + var doc = (await OpenApiDocument.LoadAsync("Models/Samples/docWithoutOperationSecurity.yaml", SettingsFixture.ReaderSettings)).Document; + var stringWriter = new StringWriter(); + doc!.SerializeAsV3(new OpenApiYamlWriter(stringWriter, new OpenApiWriterSettings { InlineLocalReferences = true })); + var actual = stringWriter.ToString(); + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral()); + var actualOperation = doc.Paths["/repairs"]!.Operations![HttpMethod.Get]; + Assert.Null(actualOperation.Security); + } + + [Fact] + public async Task SerializeDocWithEmptyOperationSecurityWorks() + { + var expected = """ + openapi: 3.0.4 + info: + title: Repair Service + version: 1.0.0 + servers: + - url: https://pluginrentu.azurewebsites.net/api + paths: + /repairs: + get: + summary: List all repairs + description: Returns a list of repairs with their details and images + operationId: listRepairs + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: object + security: [ ] + """; + + var doc = (await OpenApiDocument.LoadAsync("Models/Samples/docWithEmptyOperationSecurity.yaml", SettingsFixture.ReaderSettings)).Document; + var stringWriter = new StringWriter(); + doc!.SerializeAsV3(new OpenApiYamlWriter(stringWriter, new OpenApiWriterSettings { InlineLocalReferences = true })); + var actual = stringWriter.ToString(); + Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), actual.MakeLineBreaksEnvironmentNeutral()); + var actualOperation = doc.Paths["/repairs"]!.Operations![HttpMethod.Get]; + Assert.NotNull(actualOperation.Security); + Assert.Empty(actualOperation.Security); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Models/Samples/docWithEmptyOperationSecurity.yaml b/test/Microsoft.OpenApi.Tests/Models/Samples/docWithEmptyOperationSecurity.yaml new file mode 100644 index 000000000..1f397fedf --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/Samples/docWithEmptyOperationSecurity.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.0 +info: + title: Repair Service + version: 1.0.0 +servers: + - url: https://pluginrentu.azurewebsites.net/api +paths: + /repairs: + get: + operationId: listRepairs + summary: List all repairs + description: Returns a list of repairs with their details and images + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: object + security: [] \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/Samples/docWithoutOperationSecurity.yaml b/test/Microsoft.OpenApi.Tests/Models/Samples/docWithoutOperationSecurity.yaml new file mode 100644 index 000000000..edaf2f1f7 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/Samples/docWithoutOperationSecurity.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.0 +info: + title: Repair Service + version: 1.0.0 +servers: + - url: https://pluginrentu.azurewebsites.net/api +paths: + /repairs: + get: + operationId: listRepairs + summary: List all repairs + description: Returns a list of repairs with their details and images + responses: + '200': + description: A list of repairs + content: + application/json: + schema: + type: object \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 080f648c7..0eef70848 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1967,6 +1967,7 @@ namespace Microsoft.OpenApi.Writers public static void WriteOptionalMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.Dictionary? elements, System.Action action) where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { } public static void WriteOptionalObject(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, T? value, System.Action action) { } + public static void WriteOptionalOrEmptyCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable? elements, System.Action action) { } public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, string? value) { } public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool value, bool defaultValue = false) { } public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool? value, bool defaultValue = false) { }