From 994184b41bcd433a078cdeef75ba43d92b6b9762 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 22 Oct 2025 10:48:43 -0400 Subject: [PATCH 01/33] fix: fixes a bug where yaml null values would end up as a string "null" during roundtrip serialization Signed-off-by: Vincent Biret chore: reverts internal change Signed-off-by: Vincent Biret --- .../OpenApiYamlReader.cs | 4 +- .../YamlConverter.cs | 40 +++++++++---------- .../YamlConverterTests.cs | 29 ++++++++++++++ 3 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs diff --git a/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs index 171c3cc38..0bf2627ec 100644 --- a/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs +++ b/src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs @@ -131,9 +131,9 @@ static JsonNode LoadJsonNodesFromYamlDocument(TextReader input) { var yamlStream = new YamlStream(); yamlStream.Load(input); - if (yamlStream.Documents.Any()) + if (yamlStream.Documents.Any() && yamlStream.Documents[0].ToJsonNode() is { } jsonNode) { - return yamlStream.Documents[0].ToJsonNode(); + return jsonNode; } throw new InvalidOperationException("No documents found in the YAML stream."); diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs index e2fc5f434..b5bdb5953 100644 --- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs +++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs @@ -18,7 +18,7 @@ public static class YamlConverter /// /// The YAML stream. /// A collection of nodes representing the YAML documents in the stream. - public static IEnumerable ToJsonNode(this YamlStream yaml) + public static IEnumerable ToJsonNode(this YamlStream yaml) { return yaml.Documents.Select(x => x.ToJsonNode()); } @@ -28,7 +28,7 @@ public static IEnumerable ToJsonNode(this YamlStream yaml) /// /// The YAML document. /// A `JsonNode` representative of the YAML document. - public static JsonNode ToJsonNode(this YamlDocument yaml) + public static JsonNode? ToJsonNode(this YamlDocument yaml) { return yaml.RootNode.ToJsonNode(); } @@ -39,7 +39,7 @@ public static JsonNode ToJsonNode(this YamlDocument yaml) /// The YAML node. /// A `JsonNode` representative of the YAML node. /// Thrown for YAML that is not compatible with JSON. - public static JsonNode ToJsonNode(this YamlNode yaml) + public static JsonNode? ToJsonNode(this YamlNode yaml) { return yaml switch { @@ -110,25 +110,25 @@ private static YamlSequenceNode ToYamlSequence(this JsonArray arr) return new YamlSequenceNode(arr.Select(x => x!.ToYamlNode())); } - private static JsonValue ToJsonValue(this YamlScalarNode yaml) + private static readonly HashSet YamlNullRepresentations = new(StringComparer.Ordinal) { - switch (yaml.Style) + "~", + "null", + "Null", + "NULL" + }; + + private static JsonValue? ToJsonValue(this YamlScalarNode yaml) + { + return yaml.Style switch { - case ScalarStyle.Plain: - return decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) - ? JsonValue.Create(d) - : bool.TryParse(yaml.Value, out var b) - ? JsonValue.Create(b) - : JsonValue.Create(yaml.Value)!; - case ScalarStyle.SingleQuoted: - case ScalarStyle.DoubleQuoted: - case ScalarStyle.Literal: - case ScalarStyle.Folded: - case ScalarStyle.Any: - return JsonValue.Create(yaml.Value)!; - default: - throw new ArgumentOutOfRangeException(); - } + ScalarStyle.Plain when decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => JsonValue.Create(d), + ScalarStyle.Plain when bool.TryParse(yaml.Value, out var b) => JsonValue.Create(b), + ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => null, + ScalarStyle.Plain => JsonValue.Create(yaml.Value), + ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded or ScalarStyle.Any => JsonValue.Create(yaml.Value), + _ => throw new ArgumentOutOfRangeException(nameof(yaml)), + }; } private static YamlScalarNode ToYamlScalar(this JsonValue val) diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs new file mode 100644 index 000000000..c410d4f20 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs @@ -0,0 +1,29 @@ +using SharpYaml; +using SharpYaml.Serialization; +using Xunit; +using Microsoft.OpenApi.YamlReader; + +namespace Microsoft.OpenApi.Readers.Tests; + +public class YamlConverterTests +{ + [Theory] + [InlineData("~")] + [InlineData("null")] + [InlineData("Null")] + [InlineData("NULL")] + public void YamlNullValuesReturnNullJsonNode(string value) + { + // Given + var yamlNull = new YamlScalarNode(value) + { + Style = ScalarStyle.Plain + }; + + // When + var jsonNode = yamlNull.ToJsonNode(); + + // Then + Assert.Null(jsonNode); + } +} From e897df5b4423a82c5bf0d9cbe7e5a95b8ab7fd38 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 22 Oct 2025 12:11:45 -0400 Subject: [PATCH 02/33] tests: adds unit tests to validate the lossy nature of null conversions Signed-off-by: Vincent Biret --- .../V31Tests/OpenApiSchemaTests.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index c992f6656..32e59bfcc 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -349,6 +349,68 @@ public void DefaultEmptyCollectionShouldRoundTrip() Assert.Empty(resultingArray); } + [Fact] + public void DefaultNullIsLossyDuringRoundTripJson() + { + // Given + var serializedSchema = + """ + { + "type": ["string", "null"], + "default": null + } + """; + using var textWriter = new StringWriter(); + var writer = new OpenApiJsonWriter(textWriter); + + // When + var schema = OpenApiModelFactory.Parse(serializedSchema, OpenApiSpecVersion.OpenApi3_1, new(), out _, "json", SettingsFixture.ReaderSettings); + + Assert.Null(schema.Default); + + schema.SerializeAsV31(writer); + var roundTrippedSchema = textWriter.ToString(); + + // Then + var parsedResult = JsonNode.Parse(roundTrippedSchema); + var parsedExpected = JsonNode.Parse(serializedSchema); + Assert.False(JsonNode.DeepEquals(parsedExpected, parsedResult)); + var resultingDefault = parsedResult["default"]; + Assert.Null(resultingDefault); + } + + [Fact] + public void DefaultNullIsLossyDuringRoundTripYaml() + { + // Given + var serializedSchema = + """ + type: + - string + - 'null' + default: null + """; + using var textWriter = new StringWriter(); + var writer = new OpenApiYamlWriter(textWriter); + + // When + var schema = OpenApiModelFactory.Parse(serializedSchema, OpenApiSpecVersion.OpenApi3_1, new(), out _, "yaml", SettingsFixture.ReaderSettings); + + Assert.Null(schema.Default); + + schema.SerializeAsV31(writer); + var roundTrippedSchema = textWriter.ToString(); + + // Then + Assert.Equal( + """ + type: + - 'null' + - string + """.MakeLineBreaksEnvironmentNeutral(), + roundTrippedSchema.MakeLineBreaksEnvironmentNeutral()); + } + [Fact] public async Task SerializeV31SchemaWithMultipleTypesAsV3Works() { From 9fefb47aa74dc2acd8dbd08f7eb2e0e08515e415 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 20:37:51 +0000 Subject: [PATCH 03/33] Initial plan From e30c718fee6c8d346a300ddbe463466851df957a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Oct 2025 20:49:14 +0000 Subject: [PATCH 04/33] Fix YamlConverter adding extra quotes to string values Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../YamlConverter.cs | 25 ++- .../YamlConverterTests.cs | 179 ++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs index b5bdb5953..0c0c1bfe5 100644 --- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs +++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs @@ -133,7 +133,30 @@ ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => null, private static YamlScalarNode ToYamlScalar(this JsonValue val) { - return new YamlScalarNode(val.ToJsonString()); + // Try to get the underlying value based on its actual type + // First try to get it as a string + if (val.TryGetValue(out string? stringValue)) + { + // For string values, we need to determine if they should be quoted in YAML + // Strings that look like numbers, booleans, or null need to be quoted + // to preserve their string type when round-tripping + var needsQuoting = decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out _) || + bool.TryParse(stringValue, out _) || + YamlNullRepresentations.Contains(stringValue); + + return new YamlScalarNode(stringValue) + { + Style = needsQuoting ? ScalarStyle.DoubleQuoted : ScalarStyle.Plain + }; + } + + // For non-string values (numbers, booleans, null), use their string representation + // These should remain unquoted in YAML + var valueString = val.ToString(); + return new YamlScalarNode(valueString) + { + Style = ScalarStyle.Plain + }; } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs index c410d4f20..844b7866f 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs @@ -2,6 +2,8 @@ using SharpYaml.Serialization; using Xunit; using Microsoft.OpenApi.YamlReader; +using System.IO; +using System.Text.Json.Nodes; namespace Microsoft.OpenApi.Readers.Tests; @@ -26,4 +28,181 @@ public void YamlNullValuesReturnNullJsonNode(string value) // Then Assert.Null(jsonNode); } + + [Fact] + public void ToYamlNode_StringValue_NotQuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""fooString"": ""fooStringValue""}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("fooString: fooStringValue", yamlOutput); + Assert.DoesNotContain("\"fooStringValue\"", yamlOutput); + Assert.DoesNotContain("'fooStringValue'", yamlOutput); + } + + [Fact] + public void ToYamlNode_StringThatLooksLikeNumber_QuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""fooStringOfNumber"": ""200""}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("fooStringOfNumber: \"200\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_ActualNumber_NotQuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""actualNumber"": 200}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("actualNumber: 200", yamlOutput); + Assert.DoesNotContain("\"200\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_StringThatLooksLikeDecimal_QuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""decimalString"": ""123.45""}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("decimalString: \"123.45\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_ActualDecimal_NotQuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""actualDecimal"": 123.45}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("actualDecimal: 123.45", yamlOutput); + Assert.DoesNotContain("\"123.45\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_StringThatLooksLikeBoolean_QuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""boolString"": ""true""}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("boolString: \"true\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_ActualBoolean_NotQuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""actualBool"": true}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("actualBool: true", yamlOutput); + Assert.DoesNotContain("\"true\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_StringThatLooksLikeNull_QuotedInYaml() + { + // Arrange + var json = JsonNode.Parse(@"{""nullString"": ""null""}"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("nullString: \"null\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_MixedTypes_CorrectQuoting() + { + // Arrange + var json = JsonNode.Parse(@"{ + ""str"": ""hello"", + ""numStr"": ""42"", + ""num"": 42, + ""boolStr"": ""false"", + ""bool"": false + }"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("str: hello", yamlOutput); + Assert.Contains("numStr: \"42\"", yamlOutput); + Assert.Contains("num: 42", yamlOutput); + Assert.DoesNotContain("num: \"42\"", yamlOutput); + Assert.Contains("boolStr: \"false\"", yamlOutput); + Assert.Contains("bool: false", yamlOutput); + Assert.DoesNotContain("bool: \"false\"", yamlOutput); + } + + [Fact] + public void ToYamlNode_FromIssueExample_CorrectOutput() + { + // Arrange - Example from issue #1951 + var json = JsonNode.Parse(@"{ + ""fooString"": ""fooStringValue"", + ""fooStringOfNumber"": ""200"" + }"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Assert + Assert.Contains("fooString: fooStringValue", yamlOutput); + Assert.Contains("fooStringOfNumber: \"200\"", yamlOutput); + + // Ensure no extra quotes on regular strings + Assert.DoesNotContain("\"fooStringValue\"", yamlOutput); + Assert.DoesNotContain("'fooStringValue'", yamlOutput); + } + + private static string ConvertYamlNodeToString(YamlNode yamlNode) + { + using var ms = new MemoryStream(); + var yamlStream = new YamlStream(new YamlDocument(yamlNode)); + var writer = new StreamWriter(ms); + yamlStream.Save(writer); + writer.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(ms); + return reader.ReadToEnd(); + } } From 42c805e6363c2126a3014efde0168a4938a246ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:17:06 +0000 Subject: [PATCH 05/33] Add unit test to validate line breaks in string values are preserved Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../YamlConverterTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs index 844b7866f..03faa3dff 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs @@ -194,6 +194,39 @@ public void ToYamlNode_FromIssueExample_CorrectOutput() Assert.DoesNotContain("'fooStringValue'", yamlOutput); } + [Fact] + public void ToYamlNode_StringWithLineBreaks_PreservesLineBreaks() + { + // Arrange + var json = JsonNode.Parse(@"{ + ""multiline"": ""Line 1\nLine 2\nLine 3"", + ""description"": ""This is a description\nwith line breaks\nin it"" + }"); + + // Act + var yamlNode = json!.ToYamlNode(); + var yamlOutput = ConvertYamlNodeToString(yamlNode); + + // Convert back to JSON to verify round-tripping + var yamlStream = new YamlStream(); + using (var sr = new System.IO.StringReader(yamlOutput)) + { + yamlStream.Load(sr); + } + var jsonBack = yamlStream.Documents[0].ToJsonNode(); + + // Assert - line breaks should be preserved during round-trip + var originalMultiline = json["multiline"]?.GetValue(); + var roundTripMultiline = jsonBack?["multiline"]?.GetValue(); + Assert.Equal(originalMultiline, roundTripMultiline); + Assert.Contains("\n", roundTripMultiline); + + var originalDescription = json["description"]?.GetValue(); + var roundTripDescription = jsonBack?["description"]?.GetValue(); + Assert.Equal(originalDescription, roundTripDescription); + Assert.Contains("\n", roundTripDescription); + } + private static string ConvertYamlNodeToString(YamlNode yamlNode) { using var ms = new MemoryStream(); From 63b2b98b64633fdef3b7fafee882060a3e0808fd Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 09:49:49 -0400 Subject: [PATCH 06/33] fix: adds a null value sentinel to enable roundtrip serializations of JsonNode typed properties Signed-off-by: Vincent Biret --- .../YamlConverter.cs | 10 ++--- src/Microsoft.OpenApi/JsonNullSentinel.cs | 40 +++++++++++++++++++ .../Models/Interfaces/IOpenApiExample.cs | 2 + .../Models/Interfaces/IOpenApiHeader.cs | 2 + .../Models/Interfaces/IOpenApiParameter.cs | 2 + .../Models/Interfaces/IOpenApiSchema.cs | 4 ++ .../Models/JsonSchemaReference.cs | 2 + .../Models/OpenApiMediaType.cs | 2 + .../Models/RuntimeExpressionAnyWrapper.cs | 2 + .../Reader/ParseNodes/MapNode.cs | 8 +++- .../Writers/OpenApiWriterAnyExtensions.cs | 6 +-- .../Writers/OpenApiYamlWriter.cs | 3 +- .../V31Tests/OpenApiSchemaTests.cs | 14 ++++--- .../YamlConverterTests.cs | 4 +- .../Models/OpenApiComponentsTests.cs | 16 ++++---- .../Models/OpenApiResponseTests.cs | 6 +-- .../Models/OpenApiTagTests.cs | 8 ++-- .../PublicApi/PublicApi.approved.txt | 5 +++ .../Reader/MapNodeTests.cs | 3 +- .../Writers/OpenApiYamlWriterTests.cs | 4 +- 20 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 src/Microsoft.OpenApi/JsonNullSentinel.cs diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs index b5bdb5953..23f28f046 100644 --- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs +++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs @@ -18,7 +18,7 @@ public static class YamlConverter /// /// The YAML stream. /// A collection of nodes representing the YAML documents in the stream. - public static IEnumerable ToJsonNode(this YamlStream yaml) + public static IEnumerable ToJsonNode(this YamlStream yaml) { return yaml.Documents.Select(x => x.ToJsonNode()); } @@ -28,7 +28,7 @@ public static class YamlConverter /// /// The YAML document. /// A `JsonNode` representative of the YAML document. - public static JsonNode? ToJsonNode(this YamlDocument yaml) + public static JsonNode ToJsonNode(this YamlDocument yaml) { return yaml.RootNode.ToJsonNode(); } @@ -39,7 +39,7 @@ public static class YamlConverter /// The YAML node. /// A `JsonNode` representative of the YAML node. /// Thrown for YAML that is not compatible with JSON. - public static JsonNode? ToJsonNode(this YamlNode yaml) + public static JsonNode ToJsonNode(this YamlNode yaml) { return yaml switch { @@ -118,13 +118,13 @@ private static YamlSequenceNode ToYamlSequence(this JsonArray arr) "NULL" }; - private static JsonValue? ToJsonValue(this YamlScalarNode yaml) + private static JsonValue ToJsonValue(this YamlScalarNode yaml) { return yaml.Style switch { ScalarStyle.Plain when decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => JsonValue.Create(d), ScalarStyle.Plain when bool.TryParse(yaml.Value, out var b) => JsonValue.Create(b), - ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => null, + ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => JsonNullSentinel.JsonNull, ScalarStyle.Plain => JsonValue.Create(yaml.Value), ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded or ScalarStyle.Any => JsonValue.Create(yaml.Value), _ => throw new ArgumentOutOfRangeException(nameof(yaml)), diff --git a/src/Microsoft.OpenApi/JsonNullSentinel.cs b/src/Microsoft.OpenApi/JsonNullSentinel.cs new file mode 100644 index 000000000..68b4dc64f --- /dev/null +++ b/src/Microsoft.OpenApi/JsonNullSentinel.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Microsoft.OpenApi; + +/// +/// A sentinel value representing JSON null. +/// This can only be used for OpenAPI properties of type +/// +public static class JsonNullSentinel +{ + private const string SentinelValue = "openapi-json-null-sentinel-value-2BF93600-0FE4-4250-987A-E5DDB203E464"; + private static readonly JsonValue SentinelJsonValue = JsonValue.Create(SentinelValue)!; + /// + /// A sentinel value representing JSON null. + /// This can only be used for OpenAPI properties of type . + /// This can only be used for the root level of a JSON structure. + /// Any use outside of these constraints is unsupported and may lead to unexpected behavior. + /// Because this is returning a cloned instance, so the value can be added in a tree, reference equality checks will not work. + /// You must use the method to check for this sentinel. + /// + public static JsonValue JsonNull => (JsonValue)SentinelJsonValue.DeepClone(); + + /// + /// Determines if the given node is the JSON null sentinel. + /// + /// The JsonNode to check. + /// Whether or not the given node is the JSON null sentinel. + public static bool IsJsonNullSentinel(this JsonNode? node) + { + return node is JsonValue jsonValue && + jsonValue.GetValueKind() == JsonValueKind.String && + jsonValue.TryGetValue(out var value) && + SentinelValue.Equals(value, StringComparison.Ordinal); + } +} diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiExample.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiExample.cs index bcb85c676..12fecda19 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiExample.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiExample.cs @@ -12,6 +12,8 @@ public interface IOpenApiExample : IOpenApiDescribedElement, IOpenApiSummarizedE /// Embedded literal example. The value field and externalValue field are mutually /// exclusive. To represent examples of media types that cannot naturally represented /// in JSON or YAML, use a string value to contain the example, escaping where necessary. + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Value { get; } diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs index 46f8666aa..04c24b5b2 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiHeader.cs @@ -48,6 +48,8 @@ public interface IOpenApiHeader : IOpenApiDescribedElement, IOpenApiReadOnlyExte /// /// Example of the media type. + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Example { get; } diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs index 17db255df..312517acb 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiParameter.cs @@ -89,6 +89,8 @@ public interface IOpenApiParameter : IOpenApiDescribedElement, IOpenApiReadOnlyE /// the example value SHALL override the example provided by the schema. /// To represent examples of media types that cannot naturally be represented in JSON or YAML, /// a string value can contain the example with escaping where necessary. + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Example { get; } diff --git a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs index cc3ff51ec..f8a35d306 100644 --- a/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/Interfaces/IOpenApiSchema.cs @@ -117,6 +117,8 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte /// The default value represents what would be assumed by the consumer of the input as the value of the schema if one is not provided. /// Unlike JSON Schema, the value MUST conform to the defined type for the Schema Object defined at the same level. /// For example, if type is string, then default can be "foo" but cannot be 1. + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Default { get; } @@ -238,6 +240,8 @@ public interface IOpenApiSchema : IOpenApiDescribedElement, IOpenApiReadOnlyExte /// A free-form property to include an example of an instance for this schema. /// To represent examples that cannot be naturally represented in JSON or YAML, /// a string value can be used to contain the example with escaping where necessary. + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Example { get; } diff --git a/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs b/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs index 1e7f788df..bd758ec8e 100644 --- a/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs +++ b/src/Microsoft.OpenApi/Models/JsonSchemaReference.cs @@ -17,6 +17,8 @@ public class JsonSchemaReference : OpenApiReferenceWithDescription /// /// A default value which by default SHOULD override that of the referenced component. /// If the referenced object-type does not allow a default field, then this field has no effect. + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Default { get; set; } diff --git a/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs b/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs index b901eacd1..6a46c60a1 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs @@ -21,6 +21,8 @@ public class OpenApiMediaType : IOpenApiSerializable, IOpenApiExtensible /// /// Example of the media type. /// The example object SHOULD be in the correct format as specified by the media type. + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Example { get; set; } diff --git a/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs b/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs index 5798308c1..0fdc87eaa 100644 --- a/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs +++ b/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs @@ -29,6 +29,8 @@ public RuntimeExpressionAnyWrapper(RuntimeExpressionAnyWrapper runtimeExpression /// /// Gets/Sets the + /// You must use the method to check whether Default was assigned a null value in the document. + /// Assign to use get null as a serialized value. /// public JsonNode? Any { diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index ca89b9ea5..42978df46 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -30,15 +30,19 @@ public MapNode(ParsingContext context, JsonNode node) : base( _node = mapNode; _nodes = _node.Where(p => p.Value is not null).OfType>().Select(p => new PropertyNode(Context, p.Key, p.Value)).ToList(); + _nodes.AddRange(_node.Where(p => p.Value is null).Select(p => new PropertyNode(Context, p.Key, JsonNullSentinel.JsonNull))); } public PropertyNode? this[string key] { get { - if (_node.TryGetPropertyValue(key, out var node) && node is not null) + if (_node.TryGetPropertyValue(key, out var node)) { - return new(Context, key, node); + if (node is not null) + return new(Context, key, node); + else + return new(Context, key, JsonNullSentinel.JsonNull); } return null; diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs index 34d0d9803..6116b5b11 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs @@ -51,7 +51,7 @@ public static void WriteAny(this IOpenApiWriter writer, JsonNode? node) { Utils.CheckArgumentNull(writer); - if (node == null) + if (node == null || node.IsJsonNullSentinel()) { writer.WriteNull(); return; @@ -91,7 +91,7 @@ private static void WriteArray(this IOpenApiWriter writer, JsonArray? array) { writer.WriteAny(item); } - } + } writer.WriteEndArray(); } @@ -124,7 +124,7 @@ private static void WritePrimitive(this IOpenApiWriter writer, JsonValue jsonVal else if (jsonValue.TryGetValue(out TimeOnly timeOnlyValue)) writer.WriteValue(timeOnlyValue.ToString("o", CultureInfo.InvariantCulture)); #endif - else if (jsonValue.TryGetValue(out bool boolValue)) + else if (jsonValue.TryGetValue(out bool boolValue)) writer.WriteValue(boolValue); // write number values else if (jsonValue.TryGetValue(out decimal decimalValue)) diff --git a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs index fff9d773e..999c672cc 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs @@ -275,9 +275,8 @@ private void WriteChompingIndicator(string? value) /// public override void WriteNull() { - // YAML allows null value to be represented by either nothing or the word null. - // We will write nothing here. WriteValueSeparator(); + Writer.Write("null"); } /// diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs index 32e59bfcc..2b7f227e5 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json.Nodes; @@ -11,7 +13,6 @@ using Microsoft.OpenApi.Reader; using Microsoft.OpenApi.Tests; using Xunit; -using System; namespace Microsoft.OpenApi.Readers.Tests.V31Tests { @@ -366,16 +367,16 @@ public void DefaultNullIsLossyDuringRoundTripJson() // When var schema = OpenApiModelFactory.Parse(serializedSchema, OpenApiSpecVersion.OpenApi3_1, new(), out _, "json", SettingsFixture.ReaderSettings); - Assert.Null(schema.Default); + Assert.True(schema.Default.IsJsonNullSentinel()); schema.SerializeAsV31(writer); var roundTrippedSchema = textWriter.ToString(); // Then - var parsedResult = JsonNode.Parse(roundTrippedSchema); + var parsedResult = Assert.IsType(JsonNode.Parse(roundTrippedSchema)); var parsedExpected = JsonNode.Parse(serializedSchema); Assert.False(JsonNode.DeepEquals(parsedExpected, parsedResult)); - var resultingDefault = parsedResult["default"]; + Assert.True(parsedResult.TryGetPropertyValue("default", out var resultingDefault)); Assert.Null(resultingDefault); } @@ -396,7 +397,7 @@ public void DefaultNullIsLossyDuringRoundTripYaml() // When var schema = OpenApiModelFactory.Parse(serializedSchema, OpenApiSpecVersion.OpenApi3_1, new(), out _, "yaml", SettingsFixture.ReaderSettings); - Assert.Null(schema.Default); + Assert.True(schema.Default.IsJsonNullSentinel()); schema.SerializeAsV31(writer); var roundTrippedSchema = textWriter.ToString(); @@ -407,6 +408,7 @@ public void DefaultNullIsLossyDuringRoundTripYaml() type: - 'null' - string + default: null """.MakeLineBreaksEnvironmentNeutral(), roundTrippedSchema.MakeLineBreaksEnvironmentNeutral()); } @@ -663,7 +665,7 @@ public void ParseSchemaExampleWithPrimitivesWorks() var actual2 = textWriter.ToString(); Assert.Equal(expected2.MakeLineBreaksEnvironmentNeutral(), actual2.MakeLineBreaksEnvironmentNeutral()); } - + [Theory] [InlineData(JsonSchemaType.Integer | JsonSchemaType.String, new[] { "integer", "string" })] [InlineData(JsonSchemaType.Integer | JsonSchemaType.Null, new[] { "integer", "null" })] diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs index c410d4f20..ad8f2b07d 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs @@ -1,7 +1,7 @@ +using Microsoft.OpenApi.YamlReader; using SharpYaml; using SharpYaml.Serialization; using Xunit; -using Microsoft.OpenApi.YamlReader; namespace Microsoft.OpenApi.Readers.Tests; @@ -24,6 +24,6 @@ public void YamlNullValuesReturnNullJsonNode(string value) var jsonNode = yamlNull.ToJsonNode(); // Then - Assert.Null(jsonNode); + Assert.True(jsonNode.IsJsonNullSentinel()); } } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs index 190da253c..3a1b9fa30 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs @@ -72,7 +72,7 @@ public class OpenApiComponentsTests { Type = JsonSchemaType.Integer }, - ["property3"] = new OpenApiSchemaReference("schema2", null) + ["property3"] = new OpenApiSchemaReference("schema2", null) } }, ["schema2"] = new OpenApiSchema() @@ -215,7 +215,7 @@ public class OpenApiComponentsTests }, ["property3"] = new OpenApiSchemaReference("schema2", null) } - }, + }, ["schema2"] = new OpenApiSchema() { @@ -527,16 +527,16 @@ public async Task SerializeBrokenComponentsAsYamlV3Works() schemas: schema1: type: string - schema2: - schema3: + schema2: null + schema3: null schema4: type: string allOf: - - - - + - null + - null - type: string - - - - + - null + - null """; // Act diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs index 15c392466..a8902ef14 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs @@ -31,7 +31,7 @@ public class OpenApiResponseTests Extensions = new Dictionary() { ["myextension"] = new JsonNodeExtension("myextensionvalue"), - }, + }, } }, Headers = new Dictionary @@ -138,7 +138,7 @@ public class OpenApiResponseTests { ["text/plain"] = new OpenApiMediaType { - Schema = new OpenApiSchema() + Schema = new OpenApiSchema() { Type = JsonSchemaType.Array, Items = new OpenApiSchemaReference("customType", null) @@ -178,7 +178,7 @@ public async Task SerializeBasicResponseWorks( // Arrange var expected = format == OpenApiConstants.Json ? @"{ ""description"": null -}" : @"description: "; +}" : @"description: null"; // Act var actual = await BasicResponse.SerializeAsync(version, format); diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs index 2110e473b..16a486878 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs @@ -112,7 +112,7 @@ public async Task SerializeAdvancedTagAsV3YamlWithoutReferenceWorks() externalDocs: description: Find more info here url: https://example.com - x-tag-extension: + x-tag-extension: null """; // Act @@ -139,7 +139,7 @@ public async Task SerializeAdvancedTagAsV2YamlWithoutReferenceWorks() externalDocs: description: Find more info here url: https://example.com - x-tag-extension: + x-tag-extension: null """; // Act @@ -199,7 +199,7 @@ public async Task SerializeAdvancedTagAsV3YamlWorks() externalDocs: description: Find more info here url: https://example.com -x-tag-extension:"; +x-tag-extension: null"; // Act AdvancedTag.SerializeAsV3(writer); @@ -224,7 +224,7 @@ public async Task SerializeAdvancedTagAsV2YamlWorks() externalDocs: description: Find more info here url: https://example.com -x-tag-extension:"; +x-tag-extension: null"; // Act AdvancedTag.SerializeAsV2(writer); diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 87276efce..39cfb8ccc 100644 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -325,6 +325,11 @@ namespace Microsoft.OpenApi public System.Text.Json.Nodes.JsonNode Node { get; } public void Write(Microsoft.OpenApi.IOpenApiWriter writer, Microsoft.OpenApi.OpenApiSpecVersion specVersion) { } } + public static class JsonNullSentinel + { + public static System.Text.Json.Nodes.JsonValue JsonNull { get; } + public static bool IsJsonNullSentinel(this System.Text.Json.Nodes.JsonNode? node) { } + } public class JsonPointer { public JsonPointer(string pointer) { } diff --git a/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs b/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs index 25151e461..36065d42e 100644 --- a/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs @@ -13,6 +13,7 @@ public void DoesNotFailOnNullValue() var mapNode = new MapNode(new ParsingContext(new()), jsonNode); Assert.NotNull(mapNode); - Assert.Empty(mapNode); + Assert.Single(mapNode); + Assert.True(mapNode["key"].Value.JsonNode.IsJsonNullSentinel()); } } diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs index 5cf55ffe9..5f36e8d48 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs @@ -171,7 +171,7 @@ public static IEnumerable WriteMapAsYamlShouldMatchExpectedTestCasesCo property6: -5 property7: true property8: 'true' - property9: + property9: null property10: 'null' property11: '' """ @@ -384,7 +384,7 @@ public void WriteInlineSchema() """; var outputString = new StringWriter(CultureInfo.InvariantCulture); - var writer = new OpenApiYamlWriter(outputString, new() { InlineLocalReferences = true } ); + var writer = new OpenApiYamlWriter(outputString, new() { InlineLocalReferences = true }); // Act doc.SerializeAsV3(writer); From c6a3a107288363bc3b921d3b65945948ed5f3ab0 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:07:08 -0400 Subject: [PATCH 07/33] chore: Potential fix for code scanning alert no. 2327: Missed ternary opportunity Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 42978df46..22fd56c28 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -39,10 +39,9 @@ public PropertyNode? this[string key] { if (_node.TryGetPropertyValue(key, out var node)) { - if (node is not null) - return new(Context, key, node); - else - return new(Context, key, JsonNullSentinel.JsonNull); + return node is not null + ? new(Context, key, node) + : new(Context, key, JsonNullSentinel.JsonNull); } return null; From 67789f3df47cfbab32b7150545fa967b0e0f3667 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:20:00 -0400 Subject: [PATCH 08/33] chore: avoid systematically cloning to reduce performance impact Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi.YamlReader/YamlConverter.cs | 2 +- src/Microsoft.OpenApi/JsonNullSentinel.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs index 23f28f046..df51750fd 100644 --- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs +++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs @@ -124,7 +124,7 @@ private static JsonValue ToJsonValue(this YamlScalarNode yaml) { ScalarStyle.Plain when decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => JsonValue.Create(d), ScalarStyle.Plain when bool.TryParse(yaml.Value, out var b) => JsonValue.Create(b), - ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => JsonNullSentinel.JsonNull, + ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => (JsonValue)JsonNullSentinel.JsonNull.DeepClone(), ScalarStyle.Plain => JsonValue.Create(yaml.Value), ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded or ScalarStyle.Any => JsonValue.Create(yaml.Value), _ => throw new ArgumentOutOfRangeException(nameof(yaml)), diff --git a/src/Microsoft.OpenApi/JsonNullSentinel.cs b/src/Microsoft.OpenApi/JsonNullSentinel.cs index 68b4dc64f..38a71b6ab 100644 --- a/src/Microsoft.OpenApi/JsonNullSentinel.cs +++ b/src/Microsoft.OpenApi/JsonNullSentinel.cs @@ -20,10 +20,10 @@ public static class JsonNullSentinel /// This can only be used for OpenAPI properties of type . /// This can only be used for the root level of a JSON structure. /// Any use outside of these constraints is unsupported and may lead to unexpected behavior. - /// Because this is returning a cloned instance, so the value can be added in a tree, reference equality checks will not work. + /// Because the value might be cloned, so the value can be added in a tree, reference equality checks will not work. /// You must use the method to check for this sentinel. /// - public static JsonValue JsonNull => (JsonValue)SentinelJsonValue.DeepClone(); + public static JsonValue JsonNull => SentinelJsonValue; /// /// Determines if the given node is the JSON null sentinel. @@ -32,7 +32,8 @@ public static class JsonNullSentinel /// Whether or not the given node is the JSON null sentinel. public static bool IsJsonNullSentinel(this JsonNode? node) { - return node is JsonValue jsonValue && + return node == SentinelJsonValue || + node is JsonValue jsonValue && jsonValue.GetValueKind() == JsonValueKind.String && jsonValue.TryGetValue(out var value) && SentinelValue.Equals(value, StringComparison.Ordinal); From c5d9330768a5bace9d56285cdef5dd736f1da17d Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:21:26 -0400 Subject: [PATCH 09/33] tests: adds unit test for the JsonNUll sentinel Signed-off-by: Vincent Biret --- .../JsonNullSentinelTests.cs | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs diff --git a/test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs b/test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs new file mode 100644 index 000000000..63df56ad9 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Nodes; +using Xunit; + +namespace Microsoft.OpenApi.Tests; + +public class JsonNullSentinelTests +{ + [Fact] + public void JsonNull_ReturnsJsonValueWithSentinelString() + { + // Act + var result = JsonNullSentinel.JsonNull; + + // Assert + Assert.NotNull(result); + Assert.Equal(JsonValueKind.String, result.GetValueKind()); + } + + [Fact] + public void JsonNull_ReturnsSameInstancesEachTime() + { + // Act + var result1 = JsonNullSentinel.JsonNull; + var result2 = JsonNullSentinel.JsonNull; + + // Assert + Assert.Same(result1, result2); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsTrueForJsonNullSentinel() + { + // Arrange + var sentinel = JsonNullSentinel.JsonNull; + + // Act + var result = sentinel.IsJsonNullSentinel(); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsTrueForMultipleSentinelInstances() + { + // Arrange + var sentinel1 = JsonNullSentinel.JsonNull; + var sentinel2 = JsonNullSentinel.JsonNull; + + // Act & Assert + Assert.True(sentinel1.IsJsonNullSentinel()); + Assert.True(sentinel2.IsJsonNullSentinel()); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForNull() + { + // Arrange + JsonNode nullNode = null; + + // Act + var result = nullNode.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForJsonNull() + { + // Arrange + var jsonNull = JsonValue.Create((string)null); + + // Act + var result = jsonNull.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForRegularString() + { + // Arrange + var regularString = JsonValue.Create("regular string"); + + // Act + var result = regularString.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForEmptyString() + { + // Arrange + var emptyString = JsonValue.Create(""); + + // Act + var result = emptyString.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForNumber() + { + // Arrange + var number = JsonValue.Create(42); + + // Act + var result = number.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForBoolean() + { + // Arrange + var boolean = JsonValue.Create(true); + + // Act + var result = boolean.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForJsonObject() + { + // Arrange + var jsonObject = new JsonObject(); + + // Act + var result = jsonObject.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForJsonArray() + { + // Arrange + var jsonArray = new JsonArray(); + + // Act + var result = jsonArray.IsJsonNullSentinel(); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsJsonNullSentinel_ReturnsFalseForSimilarStrings() + { + // Arrange + var similarString1 = JsonValue.Create("openapi-json-null-sentinel-value"); + var similarString2 = JsonValue.Create("openapi-json-null-sentinel-value-2BF93600-0FE4-4250-987A-E5DDB203E465"); + var similarString3 = JsonValue.Create("OPENAPI-JSON-NULL-SENTINEL-VALUE-2BF93600-0FE4-4250-987A-E5DDB203E464"); + + // Act & Assert + Assert.False(similarString1.IsJsonNullSentinel()); + Assert.False(similarString2.IsJsonNullSentinel()); + Assert.False(similarString3.IsJsonNullSentinel()); + } + + [Fact] + public void IsJsonNullSentinel_IsCaseSensitive() + { + // Arrange + var lowercaseString = JsonValue.Create("openapi-json-null-sentinel-value-2bf93600-0fe4-4250-987a-e5ddb203e464"); + var uppercaseString = JsonValue.Create("OPENAPI-JSON-NULL-SENTINEL-VALUE-2BF93600-0FE4-4250-987A-E5DDB203E464"); + + // Act & Assert + Assert.False(lowercaseString.IsJsonNullSentinel()); + Assert.False(uppercaseString.IsJsonNullSentinel()); + } + + [Fact] + public void JsonNullSentinel_CanBeAddedToJsonObject() + { + // Arrange + var jsonObject = new JsonObject(); + var sentinel = JsonNullSentinel.JsonNull; + + // Act + jsonObject["test"] = sentinel; + + // Assert + Assert.True(jsonObject["test"].IsJsonNullSentinel()); + } + + [Fact] + public void JsonNullSentinel_CanBeAddedToJsonArray() + { + // Arrange + var jsonArray = new JsonArray(); + var sentinel = JsonNullSentinel.JsonNull.DeepClone(); + + // Act + jsonArray.Add(sentinel); + + // Assert + Assert.True(jsonArray[0].IsJsonNullSentinel()); + } + + [Fact] + public void JsonNullSentinel_SerializesToString() + { + // Arrange + var sentinel = JsonNullSentinel.JsonNull; + + // Act + var serialized = sentinel.ToJsonString(); + + // Assert + Assert.Contains("openapi-json-null-sentinel-value-2BF93600-0FE4-4250-987A-E5DDB203E464", serialized); + } + + [Fact] + public void JsonNullSentinel_DeserializedFromStringIsDetected() + { + // Arrange + var sentinel = JsonNullSentinel.JsonNull; + var serialized = sentinel.ToJsonString(); + + // Act + var deserialized = JsonNode.Parse(serialized); + + // Assert + Assert.True(deserialized.IsJsonNullSentinel()); + } +} From 9f2bdc255e2de988873201de3fe2b4cf992198c4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:28:29 -0400 Subject: [PATCH 10/33] chore: avoid using bang operators Signed-off-by: Vincent Biret --- .../YamlConverterTests.cs | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs index 03faa3dff..00542103d 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs @@ -1,4 +1,4 @@ -using SharpYaml; +using SharpYaml; using SharpYaml.Serialization; using Xunit; using Microsoft.OpenApi.YamlReader; @@ -33,10 +33,10 @@ public void YamlNullValuesReturnNullJsonNode(string value) public void ToYamlNode_StringValue_NotQuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""fooString"": ""fooStringValue""}"); + var json = Assert.IsType(JsonNode.Parse(@"{""fooString"": ""fooStringValue""}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -49,10 +49,10 @@ public void ToYamlNode_StringValue_NotQuotedInYaml() public void ToYamlNode_StringThatLooksLikeNumber_QuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""fooStringOfNumber"": ""200""}"); + var json = Assert.IsType(JsonNode.Parse(@"{""fooStringOfNumber"": ""200""}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -63,10 +63,10 @@ public void ToYamlNode_StringThatLooksLikeNumber_QuotedInYaml() public void ToYamlNode_ActualNumber_NotQuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""actualNumber"": 200}"); + var json = Assert.IsType(JsonNode.Parse(@"{""actualNumber"": 200}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -78,10 +78,10 @@ public void ToYamlNode_ActualNumber_NotQuotedInYaml() public void ToYamlNode_StringThatLooksLikeDecimal_QuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""decimalString"": ""123.45""}"); + var json = Assert.IsType(JsonNode.Parse(@"{""decimalString"": ""123.45""}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -92,10 +92,10 @@ public void ToYamlNode_StringThatLooksLikeDecimal_QuotedInYaml() public void ToYamlNode_ActualDecimal_NotQuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""actualDecimal"": 123.45}"); + var json = Assert.IsType(JsonNode.Parse(@"{""actualDecimal"": 123.45}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -107,10 +107,10 @@ public void ToYamlNode_ActualDecimal_NotQuotedInYaml() public void ToYamlNode_StringThatLooksLikeBoolean_QuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""boolString"": ""true""}"); + var json = Assert.IsType(JsonNode.Parse(@"{""boolString"": ""true""}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -121,10 +121,10 @@ public void ToYamlNode_StringThatLooksLikeBoolean_QuotedInYaml() public void ToYamlNode_ActualBoolean_NotQuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""actualBool"": true}"); + var json = Assert.IsType(JsonNode.Parse(@"{""actualBool"": true}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -136,10 +136,10 @@ public void ToYamlNode_ActualBoolean_NotQuotedInYaml() public void ToYamlNode_StringThatLooksLikeNull_QuotedInYaml() { // Arrange - var json = JsonNode.Parse(@"{""nullString"": ""null""}"); + var json = Assert.IsType(JsonNode.Parse(@"{""nullString"": ""null""}")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -150,16 +150,16 @@ public void ToYamlNode_StringThatLooksLikeNull_QuotedInYaml() public void ToYamlNode_MixedTypes_CorrectQuoting() { // Arrange - var json = JsonNode.Parse(@"{ + var json = Assert.IsType(JsonNode.Parse(@"{ ""str"": ""hello"", ""numStr"": ""42"", ""num"": 42, ""boolStr"": ""false"", ""bool"": false - }"); + }")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -176,13 +176,13 @@ public void ToYamlNode_MixedTypes_CorrectQuoting() public void ToYamlNode_FromIssueExample_CorrectOutput() { // Arrange - Example from issue #1951 - var json = JsonNode.Parse(@"{ + var json = Assert.IsType(JsonNode.Parse(@"{ ""fooString"": ""fooStringValue"", ""fooStringOfNumber"": ""200"" - }"); + }")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Assert @@ -198,13 +198,13 @@ public void ToYamlNode_FromIssueExample_CorrectOutput() public void ToYamlNode_StringWithLineBreaks_PreservesLineBreaks() { // Arrange - var json = JsonNode.Parse(@"{ + var json = Assert.IsType(JsonNode.Parse(@"{ ""multiline"": ""Line 1\nLine 2\nLine 3"", ""description"": ""This is a description\nwith line breaks\nin it"" - }"); + }")); // Act - var yamlNode = json!.ToYamlNode(); + var yamlNode = json.ToYamlNode(); var yamlOutput = ConvertYamlNodeToString(yamlNode); // Convert back to JSON to verify round-tripping From b4483843a3ec361bd7a250dd5fc93c6d067a4490 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:30:37 -0400 Subject: [PATCH 11/33] chore: linting Signed-off-by: Vincent Biret --- test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs index 00542103d..7ed271809 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs @@ -209,10 +209,8 @@ public void ToYamlNode_StringWithLineBreaks_PreservesLineBreaks() // Convert back to JSON to verify round-tripping var yamlStream = new YamlStream(); - using (var sr = new System.IO.StringReader(yamlOutput)) - { - yamlStream.Load(sr); - } + using var sr = new StringReader(yamlOutput); + yamlStream.Load(sr); var jsonBack = yamlStream.Documents[0].ToJsonNode(); // Assert - line breaks should be preserved during round-trip From 6ce3214ad3beb5abe6045e5aa1743db4249c1974 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:54:45 -0400 Subject: [PATCH 12/33] perf: use deep equals for comparison to reduce allocations Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/JsonNullSentinel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi/JsonNullSentinel.cs b/src/Microsoft.OpenApi/JsonNullSentinel.cs index 38a71b6ab..39d912cec 100644 --- a/src/Microsoft.OpenApi/JsonNullSentinel.cs +++ b/src/Microsoft.OpenApi/JsonNullSentinel.cs @@ -33,9 +33,9 @@ public static class JsonNullSentinel public static bool IsJsonNullSentinel(this JsonNode? node) { return node == SentinelJsonValue || - node is JsonValue jsonValue && - jsonValue.GetValueKind() == JsonValueKind.String && - jsonValue.TryGetValue(out var value) && - SentinelValue.Equals(value, StringComparison.Ordinal); + node is not null && + node.GetValueKind() == JsonValueKind.String && + // using deep equals here results in fewer allocations than TryGetValue + JsonNode.DeepEquals(SentinelJsonValue, node); } } From 17deefe78ce837ef66014fe585ba677a8b23d50f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:55:15 -0400 Subject: [PATCH 13/33] tests: adds a unit tests to validate an identical value matches the sentinel Signed-off-by: Vincent Biret --- test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs b/test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs index 63df56ad9..f181f8340 100644 --- a/test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs +++ b/test/Microsoft.OpenApi.Tests/JsonNullSentinelTests.cs @@ -160,6 +160,16 @@ public void IsJsonNullSentinel_ReturnsFalseForJsonArray() Assert.False(result); } + [Fact] + public void IsJsonNullSentinel_ReturnsTrueForIdenticalString() + { + // Arrange + var similarString1 = JsonValue.Create("openapi-json-null-sentinel-value-2BF93600-0FE4-4250-987A-E5DDB203E464"); + + // Act & Assert + Assert.True(similarString1.IsJsonNullSentinel()); + } + [Fact] public void IsJsonNullSentinel_ReturnsFalseForSimilarStrings() { From ab5f73bc18e6f929f8554346870308ea89eac184 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 11:07:30 -0400 Subject: [PATCH 14/33] chore: adds kind validation to avoid unnecessary value read Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi.YamlReader/YamlConverter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs index 0c0c1bfe5..7302d8d07 100644 --- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs +++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json; using System.Text.Json.Nodes; using SharpYaml; using SharpYaml.Serialization; @@ -135,7 +136,8 @@ private static YamlScalarNode ToYamlScalar(this JsonValue val) { // Try to get the underlying value based on its actual type // First try to get it as a string - if (val.TryGetValue(out string? stringValue)) + if (val.GetValueKind() == JsonValueKind.String && + val.TryGetValue(out string? stringValue)) { // For string values, we need to determine if they should be quoted in YAML // Strings that look like numbers, booleans, or null need to be quoted From f58aad235f904f94704aa14700aaca4ac16205af Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 11:24:45 -0400 Subject: [PATCH 15/33] perf: reduce allocations in mapnode Signed-off-by: Vincent Biret --- .../Reader/ParseNodes/MapNode.cs | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 22fd56c28..dbfc4affe 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -18,7 +18,19 @@ namespace Microsoft.OpenApi.Reader internal class MapNode : ParseNode, IEnumerable { private readonly JsonObject _node; - private readonly List _nodes; + private Dictionary? _nodes; + + private void EnsureNodesIsInitialized() + { + if (_nodes is null) + { + _nodes = _node.Where(p => p.Value is not null).OfType>().ToDictionary(p => p.Key, p => new PropertyNode(Context, p.Key, p.Value), StringComparer.Ordinal); + if (_node.Where(p => p.Value is null).ToArray() is { Length: > 0 } nullNodes) + { + _nodes.AddRange(nullNodes.Select(p => new KeyValuePair(p.Key, new PropertyNode(Context, p.Key, JsonNullSentinel.JsonNull)))); + } + } + } public MapNode(ParsingContext context, JsonNode node) : base( context, node) @@ -29,19 +41,19 @@ public MapNode(ParsingContext context, JsonNode node) : base( } _node = mapNode; - _nodes = _node.Where(p => p.Value is not null).OfType>().Select(p => new PropertyNode(Context, p.Key, p.Value)).ToList(); - _nodes.AddRange(_node.Where(p => p.Value is null).Select(p => new PropertyNode(Context, p.Key, JsonNullSentinel.JsonNull))); } public PropertyNode? this[string key] { get { - if (_node.TryGetPropertyValue(key, out var node)) + if (_node.ContainsKey(key)) { - return node is not null - ? new(Context, key, node) - : new(Context, key, JsonNullSentinel.JsonNull); + EnsureNodesIsInitialized(); + if (_nodes!.TryGetValue(key, out var propertyNode)) + { + return propertyNode; + } } return null; @@ -132,12 +144,13 @@ public override Dictionary> CreateArrayMap(Func GetEnumerator() { - return _nodes.GetEnumerator(); + EnsureNodesIsInitialized(); + return _nodes!.Values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { - return _nodes.GetEnumerator(); + return GetEnumerator(); } public override string GetRaw() From bdb5264bc41b345f9ea95924ca5ab679178b82b6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 11:45:01 -0400 Subject: [PATCH 16/33] perf: only initialize map node nodes on demand Signed-off-by: Vincent Biret --- .../Reader/ParseNodes/MapNode.cs | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index dbfc4affe..aa5c1c76e 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -18,18 +18,12 @@ namespace Microsoft.OpenApi.Reader internal class MapNode : ParseNode, IEnumerable { private readonly JsonObject _node; - private Dictionary? _nodes; - private void EnsureNodesIsInitialized() + private PropertyNode GetPropertyNodeFromJsonNode(string key, JsonNode? node) { - if (_nodes is null) - { - _nodes = _node.Where(p => p.Value is not null).OfType>().ToDictionary(p => p.Key, p => new PropertyNode(Context, p.Key, p.Value), StringComparer.Ordinal); - if (_node.Where(p => p.Value is null).ToArray() is { Length: > 0 } nullNodes) - { - _nodes.AddRange(nullNodes.Select(p => new KeyValuePair(p.Key, new PropertyNode(Context, p.Key, JsonNullSentinel.JsonNull)))); - } - } + return node is null ? + new PropertyNode(Context, key, JsonNullSentinel.JsonNull) : + new PropertyNode(Context, key, node); } public MapNode(ParsingContext context, JsonNode node) : base( @@ -47,14 +41,10 @@ public PropertyNode? this[string key] { get { - if (_node.ContainsKey(key)) - { - EnsureNodesIsInitialized(); - if (_nodes!.TryGetValue(key, out var propertyNode)) - { - return propertyNode; - } - } + if (_node.TryGetPropertyValue(key, out var value)) + { + return GetPropertyNodeFromJsonNode(key, value); + } return null; } @@ -142,10 +132,11 @@ public override Dictionary> CreateArrayMap(Func kvp.key, kvp => kvp.values); } + private List? _nodes; public IEnumerator GetEnumerator() { - EnsureNodesIsInitialized(); - return _nodes!.Values.GetEnumerator(); + _nodes ??= _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(); + return _nodes.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() From 339f61f13e118e6f08d6d052a56b5197366cc58f Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 11:46:23 -0400 Subject: [PATCH 17/33] chore; refactoring Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index aa5c1c76e..a7a57c578 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -21,9 +21,7 @@ internal class MapNode : ParseNode, IEnumerable private PropertyNode GetPropertyNodeFromJsonNode(string key, JsonNode? node) { - return node is null ? - new PropertyNode(Context, key, JsonNullSentinel.JsonNull) : - new PropertyNode(Context, key, node); + return new PropertyNode(Context, key, node ?? JsonNullSentinel.JsonNull); } public MapNode(ParsingContext context, JsonNode node) : base( From d3c758b0d4421d1da9979587dfaee91bbdee0c7c Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 13:14:24 -0400 Subject: [PATCH 18/33] perf: switches to lazy instantiation Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index a7a57c578..e461644f1 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -33,6 +33,7 @@ public MapNode(ParsingContext context, JsonNode node) : base( } _node = mapNode; + _nodes = new (() => _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(), true); } public PropertyNode? this[string key] @@ -130,11 +131,10 @@ public override Dictionary> CreateArrayMap(Func kvp.key, kvp => kvp.values); } - private List? _nodes; + private readonly Lazy> _nodes; public IEnumerator GetEnumerator() { - _nodes ??= _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(); - return _nodes.GetEnumerator(); + return _nodes.Value.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() From 1c96521c82cfa7414602e4f4da64e629b6c69c29 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 13:25:14 -0400 Subject: [PATCH 19/33] perf: removes the lazy initialization since the node is always enumerated Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index e461644f1..3217a227c 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -24,6 +24,7 @@ private PropertyNode GetPropertyNodeFromJsonNode(string key, JsonNode? node) return new PropertyNode(Context, key, node ?? JsonNullSentinel.JsonNull); } + private readonly List _nodes; public MapNode(ParsingContext context, JsonNode node) : base( context, node) { @@ -33,7 +34,7 @@ public MapNode(ParsingContext context, JsonNode node) : base( } _node = mapNode; - _nodes = new (() => _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(), true); + _nodes = _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(); } public PropertyNode? this[string key] @@ -131,10 +132,9 @@ public override Dictionary> CreateArrayMap(Func kvp.key, kvp => kvp.values); } - private readonly Lazy> _nodes; public IEnumerator GetEnumerator() { - return _nodes.Value.GetEnumerator(); + return _nodes.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() From 5aea977651167b96bed2a394066b901647d1daf6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 14:10:55 -0400 Subject: [PATCH 20/33] ci: make ratio non-absolute Signed-off-by: Vincent Biret --- .../policies/PercentageMemoryUsagePolicy.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs b/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs index 928bfa268..7468856fc 100644 --- a/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs +++ b/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs @@ -56,11 +56,11 @@ public override bool Equals(BenchmarkMemory? x, BenchmarkMemory? y) } private static double GetPercentageDifference(BenchmarkMemory x, BenchmarkMemory y) { - return Math.Truncate(Math.Abs(GetAbsoluteRatio(x, y)) * 10000) / 100; + return Math.Truncate(Math.Abs(GetRatio(x, y)) * 10000) / 100; } - private static double GetAbsoluteRatio(BenchmarkMemory x, BenchmarkMemory y) + private static double GetRatio(BenchmarkMemory x, BenchmarkMemory y) { - return Math.Abs(((double)(x.AllocatedBytes - y.AllocatedBytes))/x.AllocatedBytes); + return (double)(x.AllocatedBytes - y.AllocatedBytes) / x.AllocatedBytes; } public override string GetErrorMessage(BenchmarkMemory? x, BenchmarkMemory? y) { @@ -68,6 +68,6 @@ public override string GetErrorMessage(BenchmarkMemory? x, BenchmarkMemory? y) { return "One of the benchmarks is null."; } - return $"Allocated bytes differ: {x.AllocatedBytes} != {y.AllocatedBytes}, Ratio: {GetAbsoluteRatio(x, y)}, Allowed: {TolerancePercentagePoints}%"; + return $"Allocated bytes differ: {x.AllocatedBytes} != {y.AllocatedBytes}, Ratio: {GetRatio(x, y)}, Allowed: {TolerancePercentagePoints}%"; } } From 1de73556db82f9eb83e7ad9e14e51120f7dba597 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 14:22:24 -0400 Subject: [PATCH 21/33] ci: because I can't calculate a ratio properly... Signed-off-by: Vincent Biret --- .../resultsComparer/policies/PercentageMemoryUsagePolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs b/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs index 7468856fc..40ae2490f 100644 --- a/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs +++ b/performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs @@ -60,7 +60,7 @@ private static double GetPercentageDifference(BenchmarkMemory x, BenchmarkMemory } private static double GetRatio(BenchmarkMemory x, BenchmarkMemory y) { - return (double)(x.AllocatedBytes - y.AllocatedBytes) / x.AllocatedBytes; + return (double)(y.AllocatedBytes - x.AllocatedBytes) / x.AllocatedBytes; } public override string GetErrorMessage(BenchmarkMemory? x, BenchmarkMemory? y) { From 4e75438344847abe30b10aa12270c6320674e97c Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 14:31:38 -0400 Subject: [PATCH 22/33] ci: adds better error message Signed-off-by: Vincent Biret --- performance/resultsComparer/handlers/CompareCommandHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/performance/resultsComparer/handlers/CompareCommandHandler.cs b/performance/resultsComparer/handlers/CompareCommandHandler.cs index 1565fdf77..520c6d252 100644 --- a/performance/resultsComparer/handlers/CompareCommandHandler.cs +++ b/performance/resultsComparer/handlers/CompareCommandHandler.cs @@ -64,7 +64,7 @@ private static async Task CompareResultsAsync(string existingReportPath, st { if (!comparisonPolicy.Equals(existingBenchmarkResult.Value, newBenchmarkResult)) { - logger.LogError("Benchmark result for {ExistingBenchmarkResultKey} does not match the existing benchmark result. {ErrorMessage}", existingBenchmarkResult.Key, comparisonPolicy.GetErrorMessage(existingBenchmarkResult.Value, newBenchmarkResult)); + logger.LogError("Benchmark result for {ExistingBenchmarkResultKey} does not match the existing benchmark result (original!=new). {ErrorMessage}", existingBenchmarkResult.Key, comparisonPolicy.GetErrorMessage(existingBenchmarkResult.Value, newBenchmarkResult)); hasErrors = true; } } From dbbbf1330934bc35fb35610a6a5db65514596c48 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 14:39:04 -0400 Subject: [PATCH 23/33] perf: do not duplicate nodes when indexing Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 3217a227c..3f3ec85af 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -24,7 +24,7 @@ private PropertyNode GetPropertyNodeFromJsonNode(string key, JsonNode? node) return new PropertyNode(Context, key, node ?? JsonNullSentinel.JsonNull); } - private readonly List _nodes; + private readonly Dictionary _nodes; public MapNode(ParsingContext context, JsonNode node) : base( context, node) { @@ -34,16 +34,16 @@ public MapNode(ParsingContext context, JsonNode node) : base( } _node = mapNode; - _nodes = _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(); + _nodes = _node.ToDictionary(static p => p.Key, p => GetPropertyNodeFromJsonNode(p.Key, p.Value), StringComparer.Ordinal); } public PropertyNode? this[string key] { get { - if (_node.TryGetPropertyValue(key, out var value)) + if (_node.ContainsKey(key)) { - return GetPropertyNodeFromJsonNode(key, value); + return _nodes[key]; } return null; @@ -134,7 +134,7 @@ public override Dictionary> CreateArrayMap(Func GetEnumerator() { - return _nodes.GetEnumerator(); + return _nodes.Values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() From 199b8870c99f760d8d2288ad79c3b41fd5f9b083 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 14:40:23 -0400 Subject: [PATCH 24/33] chore: refactoring Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 3f3ec85af..186a17df8 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -41,9 +41,9 @@ public PropertyNode? this[string key] { get { - if (_node.ContainsKey(key)) + if (_nodes.TryGetValue(key, out var value)) { - return _nodes[key]; + return value; } return null; From ba1486bf50432fed4a63a28f490e6f336c51fec7 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 14:50:37 -0400 Subject: [PATCH 25/33] Revert "chore: refactoring" This reverts commit 199b8870c99f760d8d2288ad79c3b41fd5f9b083. --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 186a17df8..3f3ec85af 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -41,9 +41,9 @@ public PropertyNode? this[string key] { get { - if (_nodes.TryGetValue(key, out var value)) + if (_node.ContainsKey(key)) { - return value; + return _nodes[key]; } return null; From 1b27a26e7c835e9f6fa52d22a6b01b4689850421 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 14:50:46 -0400 Subject: [PATCH 26/33] Revert "perf: do not duplicate nodes when indexing" This reverts commit dbbbf1330934bc35fb35610a6a5db65514596c48. --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 3f3ec85af..3217a227c 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -24,7 +24,7 @@ private PropertyNode GetPropertyNodeFromJsonNode(string key, JsonNode? node) return new PropertyNode(Context, key, node ?? JsonNullSentinel.JsonNull); } - private readonly Dictionary _nodes; + private readonly List _nodes; public MapNode(ParsingContext context, JsonNode node) : base( context, node) { @@ -34,16 +34,16 @@ public MapNode(ParsingContext context, JsonNode node) : base( } _node = mapNode; - _nodes = _node.ToDictionary(static p => p.Key, p => GetPropertyNodeFromJsonNode(p.Key, p.Value), StringComparer.Ordinal); + _nodes = _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(); } public PropertyNode? this[string key] { get { - if (_node.ContainsKey(key)) + if (_node.TryGetPropertyValue(key, out var value)) { - return _nodes[key]; + return GetPropertyNodeFromJsonNode(key, value); } return null; @@ -134,7 +134,7 @@ public override Dictionary> CreateArrayMap(Func GetEnumerator() { - return _nodes.Values.GetEnumerator(); + return _nodes.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() From c24dfbd916d539344f4f1644cc73883fa76f338c Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 15:53:00 -0400 Subject: [PATCH 27/33] chore: removes unused API surface Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs | 13 ------------- .../Reader/V2/OpenApiV2Deserializer.cs | 9 +++------ test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs | 5 +++-- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs index 3217a227c..3b66d549e 100644 --- a/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi/Reader/ParseNodes/MapNode.cs @@ -37,19 +37,6 @@ public MapNode(ParsingContext context, JsonNode node) : base( _nodes = _node.Select(p => GetPropertyNodeFromJsonNode(p.Key, p.Value)).ToList(); } - public PropertyNode? this[string key] - { - get - { - if (_node.TryGetPropertyValue(key, out var value)) - { - return GetPropertyNodeFromJsonNode(key, value); - } - - return null; - } - } - public override Dictionary CreateMap(Func map, OpenApiDocument hostDocument) { var jsonMap = _node ?? throw new OpenApiReaderException($"Expected map while parsing {typeof(T).Name}", Context); diff --git a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs index 1ef067771..b312ca97c 100644 --- a/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs +++ b/src/Microsoft.OpenApi/Reader/V2/OpenApiV2Deserializer.cs @@ -19,19 +19,16 @@ private static void ParseMap( T domainObject, FixedFieldMap fixedFieldMap, PatternFieldMap patternFieldMap, - OpenApiDocument doc, - List? requiredFields = null) + OpenApiDocument doc) { if (mapNode == null) { return; } - var allFields = fixedFieldMap.Keys.Union(mapNode.Select(static x => x.Name)); - foreach (var propertyNode in allFields) + foreach (var propertyNode in mapNode) { - mapNode[propertyNode]?.ParseField(domainObject, fixedFieldMap, patternFieldMap, doc); - requiredFields?.Remove(propertyNode); + propertyNode.ParseField(domainObject, fixedFieldMap, patternFieldMap, doc); } } diff --git a/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs b/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs index 36065d42e..02068801e 100644 --- a/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Reader/MapNodeTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Nodes; +using System.Linq; +using System.Text.Json.Nodes; using Microsoft.OpenApi.Reader; using Xunit; @@ -14,6 +15,6 @@ public void DoesNotFailOnNullValue() Assert.NotNull(mapNode); Assert.Single(mapNode); - Assert.True(mapNode["key"].Value.JsonNode.IsJsonNullSentinel()); + Assert.True(mapNode.First().Value.JsonNode.IsJsonNullSentinel()); } } From 61645ae88accbb4174430236bc690df2a1c49233 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 15:55:47 -0400 Subject: [PATCH 28/33] chore: updates performance tests reports Signed-off-by: Vincent Biret --- .../performance.Descriptions-report-github.md | 22 +++--- .../performance.Descriptions-report.csv | 8 +-- .../performance.Descriptions-report.html | 22 +++--- .../performance.Descriptions-report.json | 2 +- .../performance.EmptyModels-report-github.md | 70 +++++++++---------- .../performance.EmptyModels-report.csv | 58 +++++++-------- .../performance.EmptyModels-report.html | 70 +++++++++---------- .../performance.EmptyModels-report.json | 2 +- 8 files changed, 127 insertions(+), 127 deletions(-) diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md index 5c8d571e6..3f182cfb6 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -1,18 +1,18 @@ ``` -BenchmarkDotNet v0.15.2, Linux Ubuntu 24.04.2 LTS (Noble Numbat) -AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores -.NET SDK 8.0.413 - [Host] : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX2 - ShortRun : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX2 +BenchmarkDotNet v0.15.4, Windows 11 (10.0.26200.6899) +11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores +.NET SDK 8.0.415 + [Host] : .NET 8.0.21 (8.0.21, 8.0.2125.47513), X64 RyuJIT x86-64-v4 + ShortRun : .NET 8.0.21 (8.0.21, 8.0.2125.47513), X64 RyuJIT x86-64-v4 Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------------- |---------------:|--------------:|------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 529.5 μs | 62.50 μs | 3.43 μs | 23.4375 | 3.9063 | - | 387.26 KB | -| PetStoreJson | 240.8 μs | 15.69 μs | 0.86 μs | 13.6719 | 1.9531 | - | 249.1 KB | -| GHESYaml | 1,097,576.6 μs | 100,584.42 μs | 5,513.37 μs | 26000.0000 | 20000.0000 | 3000.0000 | 384492.38 KB | -| GHESJson | 516,328.2 μs | 87,964.22 μs | 4,821.62 μs | 16000.0000 | 9000.0000 | 2000.0000 | 245957.5 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------- |-------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:| +| PetStoreYaml | 407.7 μs | 134.55 μs | 7.38 μs | 58.5938 | 7.8125 | - | 360.8 KB | +| PetStoreJson | 166.3 μs | 23.12 μs | 1.27 μs | 36.1328 | 6.8359 | - | 222.95 KB | +| GHESYaml | 896,578.2 μs | 138,441.39 μs | 7,588.44 μs | 60000.0000 | 23000.0000 | 4000.0000 | 345015.7 KB | +| GHESJson | 432,991.2 μs | 243,041.11 μs | 13,321.90 μs | 33000.0000 | 12000.0000 | 2000.0000 | 206538.29 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index 1a21885c7..36925dad9 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv @@ -1,5 +1,5 @@ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Gen1,Gen2,Allocated -PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,529.5 μs,62.50 μs,3.43 μs,23.4375,3.9063,0.0000,387.26 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,240.8 μs,15.69 μs,0.86 μs,13.6719,1.9531,0.0000,249.1 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"1,097,576.6 μs","100,584.42 μs","5,513.37 μs",26000.0000,20000.0000,3000.0000,384492.38 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"516,328.2 μs","87,964.22 μs","4,821.62 μs",16000.0000,9000.0000,2000.0000,245957.5 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,407.7 μs,134.55 μs,7.38 μs,58.5938,7.8125,0.0000,360.8 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,166.3 μs,23.12 μs,1.27 μs,36.1328,6.8359,0.0000,222.95 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"896,578.2 μs","138,441.39 μs","7,588.44 μs",60000.0000,23000.0000,4000.0000,345015.7 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"432,991.2 μs","243,041.11 μs","13,321.90 μs",33000.0000,12000.0000,2000.0000,206538.29 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index 5661bcdf8..39bab3925 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html @@ -2,7 +2,7 @@ -performance.Descriptions-20250820-142630 +performance.Descriptions-20251023-154034