diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9839c4904..a69dbcb16 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: - name: Initialize CodeQL id: init_codeql - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: queries: security-and-quality @@ -49,6 +49,6 @@ jobs: - name: Perform CodeQL Analysis id: analyze_codeql - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8a45bd5dd..13d9f224f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.3.4" + ".": "2.3.6" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 361168645..d3b2d987e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [2.3.6](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.5...v2.3.6) (2025-10-20) + + +### Bug Fixes + +* a bug where empty collections would not be serialized for default values ([4c4d257](https://github.com/microsoft/OpenAPI.NET/commit/4c4d257c0cf10d1742fae9f3961e4a6242c0ce1d)) + +## [2.3.5](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.4...v2.3.5) (2025-10-14) + + +### Bug Fixes + +* use settings for terse output in serialization extension methods ([246039b](https://github.com/microsoft/OpenAPI.NET/commit/246039bfa8a16c042a10a87126289de82d18b321)) +* use settings for terse output in serialization extension methods ([8b91278](https://github.com/microsoft/OpenAPI.NET/commit/8b912788ef18b44a083d3fd2a1d6e25c9e6e17cb)) + ## [2.3.4](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.3...v2.3.4) (2025-10-06) diff --git a/Directory.Build.props b/Directory.Build.props index c8f5a4612..6d13b2509 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,7 +12,7 @@ https://github.com/Microsoft/OpenAPI.NET © Microsoft Corporation. All rights reserved. OpenAPI .NET - 2.3.4 + 2.3.6 diff --git a/global.json b/global.json index da70a0aea..4c2f00caf 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.414" + "version": "8.0.415" } } \ No newline at end of file diff --git a/performance/resultsComparer/resultsComparer.csproj b/performance/resultsComparer/resultsComparer.csproj index 82a71aab7..c409872f6 100644 --- a/performance/resultsComparer/resultsComparer.csproj +++ b/performance/resultsComparer/resultsComparer.csproj @@ -8,12 +8,12 @@ - - - - + + + + - + diff --git a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj index 72a6485ea..7e61230e4 100644 --- a/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj +++ b/src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj @@ -29,10 +29,10 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs index 8787e7697..797ec2359 100644 --- a/src/Microsoft.OpenApi.Hidi/OpenApiService.cs +++ b/src/Microsoft.OpenApi.Hidi/OpenApiService.cs @@ -192,16 +192,17 @@ private static async Task WriteOpenApiAsync(HidiOptions options, string openApiF using var outputStream = options.Output.Create(); using var textWriter = new StreamWriter(outputStream); - var settings = new OpenApiWriterSettings + var settings = new OpenApiJsonWriterSettings { InlineLocalReferences = options.InlineLocal, - InlineExternalReferences = options.InlineExternal + InlineExternalReferences = options.InlineExternal, + Terse = options.TerseOutput }; #pragma warning disable CA1308 IOpenApiWriter writer = openApiFormat.ToLowerInvariant() switch #pragma warning restore CA1308 { - OpenApiConstants.Json => options.TerseOutput ? new(textWriter, settings, options.TerseOutput) : new OpenApiJsonWriter(textWriter, settings, false), + OpenApiConstants.Json => new OpenApiJsonWriter(textWriter, settings), OpenApiConstants.Yaml => new OpenApiYamlWriter(textWriter, settings), _ => throw new ArgumentException("Unknown format"), }; diff --git a/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj b/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj index cfd669367..2303fdedc 100644 --- a/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj +++ b/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs index 1ed0aacab..fdca97797 100755 --- a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs @@ -88,7 +88,8 @@ public static Task SerializeAsync( IOpenApiWriter writer = format.ToLowerInvariant() switch { - OpenApiConstants.Json => new OpenApiJsonWriter(streamWriter, settings, false), + OpenApiConstants.Json when settings is OpenApiJsonWriterSettings jsonSettings => new OpenApiJsonWriter(streamWriter, jsonSettings), + OpenApiConstants.Json => new OpenApiJsonWriter(streamWriter, settings), OpenApiConstants.Yaml => new OpenApiYamlWriter(streamWriter, settings), _ => throw new OpenApiException(string.Format(SRResource.OpenApiFormatNotSupported, format)), }; diff --git a/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs b/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs index feffb3b59..11dda6c59 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; +using System.ComponentModel; using System.IO; namespace Microsoft.OpenApi @@ -14,7 +16,17 @@ public class OpenApiJsonWriter : OpenApiWriterBase /// Initializes a new instance of the class. /// /// The text writer. - public OpenApiJsonWriter(TextWriter textWriter) : base(textWriter, null) + public OpenApiJsonWriter(TextWriter textWriter) : this(textWriter, (OpenApiWriterSettings?)null) + { + // this constructor is kept for binary compatibility + // TODO remove in next major version and make the settings an optional parameter in the other constructor + } + /// + /// Initializes a new instance of the class. + /// + /// Settings for controlling how the OpenAPI document will be written out. + /// The text writer. + public OpenApiJsonWriter(TextWriter textWriter, OpenApiWriterSettings? settings) : base(textWriter, settings ?? new OpenApiJsonWriterSettings()) { } @@ -34,9 +46,13 @@ public OpenApiJsonWriter(TextWriter textWriter, OpenApiJsonWriterSettings settin /// The text writer. /// Settings for controlling how the OpenAPI document will be written out. /// Setting for allowing the JSON emitted to be in terse format. - public OpenApiJsonWriter(TextWriter textWriter, OpenApiWriterSettings? settings, bool terseOutput = false) : base(textWriter, settings) + [Obsolete("Use OpenApiJsonWriter(TextWriter textWriter, OpenApiJsonWriterSettings settings) instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public OpenApiJsonWriter(TextWriter textWriter, OpenApiWriterSettings? settings, bool terseOutput) : base(textWriter, settings) { _produceTerseOutput = terseOutput; + // this constructor is kept for binary compatibility, terse information should be read from the settings to avoid fork APIs. + // TODO remove in next major version } /// diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs index 7d80480d0..e3848ec69 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs @@ -55,10 +55,7 @@ protected OpenApiWriterBase(TextWriter textWriter, OpenApiWriterSettings? settin Writer.NewLine = "\n"; Scopes = new(); - if (settings == null) - { - settings = new(); - } + settings ??= new(); Settings = settings; } diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs index f2e4aa5a9..f346dddc4 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs @@ -141,9 +141,9 @@ public static void WriteOptionalObject( { if (value != null) { - if (value is IEnumerable values && !values.GetEnumerator().MoveNext()) + if (value is IEnumerable values && value is not JsonArray && !values.GetEnumerator().MoveNext()) { - return; // Don't render optional empty collections + return; // Don't render optional empty collections except for the Default properties which are JsonArray } writer.WriteRequiredObject(name, value, action); diff --git a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj index 33656177d..5b5b5fedf 100644 --- a/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj +++ b/test/Microsoft.OpenApi.Hidi.Tests/Microsoft.OpenApi.Hidi.Tests.csproj @@ -17,7 +17,7 @@ - + diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 8e19f7370..39befa33f 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -19,9 +19,9 @@ - + - + diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index 72fb153dd..c992f6656 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -315,6 +315,40 @@ public void CloningSchemaWithExamplesAndEnumsShouldSucceed() Assert.Equivalent(6, clone.Default.GetValue()); } + [Fact] + public void DefaultEmptyCollectionShouldRoundTrip() + { + // Given + var serializedSchema = + """ + { + "type": "array", + "items": { + "type": "string", + "default": [] + } + } + """; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + // When + var schema = OpenApiModelFactory.Parse(serializedSchema, OpenApiSpecVersion.OpenApi3_1, new(), out _, "json", SettingsFixture.ReaderSettings); + + var deserializedArray = Assert.IsType(schema.Items.Default); + Assert.Empty(deserializedArray); + + schema.SerializeAsV31(writer); + var roundTrippedSchema = textWriter.ToString(); + + // Then + var parsedResult = JsonNode.Parse(roundTrippedSchema); + var parsedExpected = JsonNode.Parse(serializedSchema); + Assert.True(JsonNode.DeepEquals(parsedExpected, parsedResult)); + var resultingArray = Assert.IsType(parsedResult["items"]?["default"]); + Assert.Empty(resultingArray); + } + [Fact] public async Task SerializeV31SchemaWithMultipleTypesAsV3Works() { diff --git a/test/Microsoft.OpenApi.Tests/Extensions/OpenApiSerializableExtensionsTests.cs b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiSerializableExtensionsTests.cs new file mode 100644 index 000000000..a1e323ebf --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Extensions/OpenApiSerializableExtensionsTests.cs @@ -0,0 +1,93 @@ +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Extensions; + +public class OpenApiSerializableExtensionsTests +{ + [Fact] + public async Task UsesTheTerseOutputInformationFromSettingsTrue() + { + var parameter = new OpenApiParameter + { + Name = "param1", + In = ParameterLocation.Query, + Description = "A sample parameter", + Required = false, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }; + + var settings = new OpenApiJsonWriterSettings + { + Terse = true + }; + + using var stream = new MemoryStream(); + await parameter.SerializeAsync(stream, OpenApiSpecVersion.OpenApi3_1, OpenApiConstants.Json, settings); + + stream.Position = 0; + using var reader = new StreamReader(stream); + var output = await reader.ReadToEndAsync(); + + Assert.Equal("{\"name\":\"param1\",\"in\":\"query\",\"description\":\"A sample parameter\",\"schema\":{\"type\":\"string\"}}", output); + } + + [Fact] + public async Task UsesTheTerseOutputInformationFromSettingsFalse() + { + var parameter = new OpenApiParameter + { + Name = "param1", + In = ParameterLocation.Query, + Description = "A sample parameter", + Required = false, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }; + + var settings = new OpenApiJsonWriterSettings + { + Terse = false + }; + + using var stream = new MemoryStream(); + await parameter.SerializeAsync(stream, OpenApiSpecVersion.OpenApi3_1, OpenApiConstants.Json, settings); + + stream.Position = 0; + using var reader = new StreamReader(stream); + var output = await reader.ReadToEndAsync(); + + Assert.Equal("{\n \"name\": \"param1\",\n \"in\": \"query\",\n \"description\": \"A sample parameter\",\n \"schema\": {\n \"type\": \"string\"\n }\n}", output); + } + + [Fact] + public async Task UsesTheTerseOutputInformationFromSettingsNoSettings() + { + var parameter = new OpenApiParameter + { + Name = "param1", + In = ParameterLocation.Query, + Description = "A sample parameter", + Required = false, + Schema = new OpenApiSchema + { + Type = JsonSchemaType.String + } + }; + + using var stream = new MemoryStream(); + await parameter.SerializeAsync(stream, OpenApiSpecVersion.OpenApi3_1, OpenApiConstants.Json, null); + + stream.Position = 0; + using var reader = new StreamReader(stream); + var output = await reader.ReadToEndAsync(); + + Assert.Equal("{\n \"name\": \"param1\",\n \"in\": \"query\",\n \"description\": \"A sample parameter\",\n \"schema\": {\n \"type\": \"string\"\n }\n}", output); + } +} diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 763db67d6..98b459aa6 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -13,12 +13,12 @@ - - + + - + - + diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 408ddb15e..87276efce 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -795,7 +795,10 @@ namespace Microsoft.OpenApi { public OpenApiJsonWriter(System.IO.TextWriter textWriter) { } public OpenApiJsonWriter(System.IO.TextWriter textWriter, Microsoft.OpenApi.OpenApiJsonWriterSettings settings) { } - public OpenApiJsonWriter(System.IO.TextWriter textWriter, Microsoft.OpenApi.OpenApiWriterSettings? settings, bool terseOutput = false) { } + public OpenApiJsonWriter(System.IO.TextWriter textWriter, Microsoft.OpenApi.OpenApiWriterSettings? settings) { } + [System.Obsolete("Use OpenApiJsonWriter(TextWriter textWriter, OpenApiJsonWriterSettings settings) " + + "instead.")] + public OpenApiJsonWriter(System.IO.TextWriter textWriter, Microsoft.OpenApi.OpenApiWriterSettings? settings, bool terseOutput) { } protected override int BaseIndentation { get; } public override void WriteEndArray() { } public override void WriteEndObject() { }