Skip to content

Commit 2322792

Browse files
committed
Merge branch 'main' into chore/upstream-sync
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
2 parents dcf7956 + 537dc98 commit 2322792

29 files changed

+735
-76
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "2.3.6"
2+
".": "2.3.7"
33
}

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
## [2.3.7](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.6...v2.3.7) (2025-10-24)
4+
5+
6+
### Bug Fixes
7+
8+
* adds a null value sentinel to enable roundtrip serializations of JsonNode typed properties ([337c6eb](https://github.com/microsoft/OpenAPI.NET/commit/337c6eb08a9cc99bfbe2cd08fed61678030b8b8b))
9+
* adds a null value sentinel to enable roundtrip serializations of JsonNode typed properties ([63b2b98](https://github.com/microsoft/OpenAPI.NET/commit/63b2b98b64633fdef3b7fafee882060a3e0808fd))
10+
* fixes a bug where yaml null values would end up as a string "null" during roundtrip serialization ([6e62de2](https://github.com/microsoft/OpenAPI.NET/commit/6e62de205f0a5d58b385b4536dc30035a9977054))
11+
* fixes a bug where yaml null values would end up as a string "null" during roundtrip serialization ([994184b](https://github.com/microsoft/OpenAPI.NET/commit/994184b41bcd433a078cdeef75ba43d92b6b9762))
12+
* YamlConverter adding extra quotes to string values when converting from JSON to YAML ([ccfebc8](https://github.com/microsoft/OpenAPI.NET/commit/ccfebc828c2793b00faf3d5b12bd95bc68901104))
13+
14+
15+
### Performance Improvements
16+
17+
* do not duplicate nodes when indexing ([dbbbf13](https://github.com/microsoft/OpenAPI.NET/commit/dbbbf1330934bc35fb35610a6a5db65514596c48))
18+
* only initialize map node nodes on demand ([bdb5264](https://github.com/microsoft/OpenAPI.NET/commit/bdb5264bc41b345f9ea95924ca5ab679178b82b6))
19+
* reduce allocations in mapnode ([f58aad2](https://github.com/microsoft/OpenAPI.NET/commit/f58aad235f904f94704aa14700aaca4ac16205af))
20+
* removes the lazy initialization since the node is always enumerated ([1c96521](https://github.com/microsoft/OpenAPI.NET/commit/1c96521c82cfa7414602e4f4da64e629b6c69c29))
21+
* switches to lazy instantiation ([d3c758b](https://github.com/microsoft/OpenAPI.NET/commit/d3c758b0d4421d1da9979587dfaee91bbdee0c7c))
22+
* use deep equals for comparison to reduce allocations ([6ce3214](https://github.com/microsoft/OpenAPI.NET/commit/6ce3214ad3beb5abe6045e5aa1743db4249c1974))
23+
324
## [2.3.6](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.5...v2.3.6) (2025-10-20)
425

526

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<PackageProjectUrl>https://github.com/Microsoft/OpenAPI.NET</PackageProjectUrl>
1313
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
1414
<PackageTags>OpenAPI .NET</PackageTags>
15-
<Version>2.3.6</Version>
15+
<Version>2.3.7</Version>
1616
</PropertyGroup>
1717
<!-- https://github.com/clairernovotny/DeterministicBuilds#deterministic-builds -->
1818
<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">

performance/resultsComparer/handlers/CompareCommandHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private static async Task<int> CompareResultsAsync(string existingReportPath, st
6464
{
6565
if (!comparisonPolicy.Equals(existingBenchmarkResult.Value, newBenchmarkResult))
6666
{
67-
logger.LogError("Benchmark result for {ExistingBenchmarkResultKey} does not match the existing benchmark result. {ErrorMessage}", existingBenchmarkResult.Key, comparisonPolicy.GetErrorMessage(existingBenchmarkResult.Value, newBenchmarkResult));
67+
logger.LogError("Benchmark result for {ExistingBenchmarkResultKey} does not match the existing benchmark result (original!=new). {ErrorMessage}", existingBenchmarkResult.Key, comparisonPolicy.GetErrorMessage(existingBenchmarkResult.Value, newBenchmarkResult));
6868
hasErrors = true;
6969
}
7070
}

performance/resultsComparer/policies/PercentageMemoryUsagePolicy.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,18 @@ public override bool Equals(BenchmarkMemory? x, BenchmarkMemory? y)
5656
}
5757
private static double GetPercentageDifference(BenchmarkMemory x, BenchmarkMemory y)
5858
{
59-
return Math.Truncate(Math.Abs(GetAbsoluteRatio(x, y)) * 10000) / 100;
59+
return Math.Truncate(Math.Abs(GetRatio(x, y)) * 10000) / 100;
6060
}
61-
private static double GetAbsoluteRatio(BenchmarkMemory x, BenchmarkMemory y)
61+
private static double GetRatio(BenchmarkMemory x, BenchmarkMemory y)
6262
{
63-
return Math.Abs(((double)(x.AllocatedBytes - y.AllocatedBytes))/x.AllocatedBytes);
63+
return (double)(y.AllocatedBytes - x.AllocatedBytes) / x.AllocatedBytes;
6464
}
6565
public override string GetErrorMessage(BenchmarkMemory? x, BenchmarkMemory? y)
6666
{
6767
if (x is null || y is null)
6868
{
6969
return "One of the benchmarks is null.";
7070
}
71-
return $"Allocated bytes differ: {x.AllocatedBytes} != {y.AllocatedBytes}, Ratio: {GetAbsoluteRatio(x, y)}, Allowed: {TolerancePercentagePoints}%";
71+
return $"Allocated bytes differ: {x.AllocatedBytes} != {y.AllocatedBytes}, Ratio: {GetRatio(x, y)}, Allowed: {TolerancePercentagePoints}%";
7272
}
7373
}

src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<PrivateAssets>all</PrivateAssets>
3939
</PackageReference>
4040
<PackageReference Include="System.CommandLine" Version="2.0.0-beta5.25306.1" />
41-
<PackageReference Include="Microsoft.OData.Edm" Version="8.4.0" />
41+
<PackageReference Include="Microsoft.OData.Edm" Version="8.4.2" />
4242
<PackageReference Include="Microsoft.OpenApi.OData" Version="2.0.0" />
4343
<PackageReference Include="Microsoft.OpenApi.ApiManifest" Version="2.0.0-preview5" />
4444
<PackageReference Include="System.CommandLine.Hosting" Version="0.4.0-alpha.25306.1" />

src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ static JsonNode LoadJsonNodesFromYamlDocument(TextReader input)
131131
{
132132
var yamlStream = new YamlStream();
133133
yamlStream.Load(input);
134-
if (yamlStream.Documents.Any())
134+
if (yamlStream.Documents.Any() && yamlStream.Documents[0].ToJsonNode() is { } jsonNode)
135135
{
136-
return yamlStream.Documents[0].ToJsonNode();
136+
return jsonNode;
137137
}
138138

139139
throw new InvalidOperationException("No documents found in the YAML stream.");

src/Microsoft.OpenApi.YamlReader/YamlConverter.cs

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Globalization;
44
using System.Linq;
5+
using System.Text.Json;
56
using System.Text.Json.Nodes;
67
using SharpYaml;
78
using SharpYaml.Serialization;
@@ -110,30 +111,54 @@ private static YamlSequenceNode ToYamlSequence(this JsonArray arr)
110111
return new YamlSequenceNode(arr.Select(x => x!.ToYamlNode()));
111112
}
112113

114+
private static readonly HashSet<string> YamlNullRepresentations = new(StringComparer.Ordinal)
115+
{
116+
"~",
117+
"null",
118+
"Null",
119+
"NULL"
120+
};
121+
113122
private static JsonValue ToJsonValue(this YamlScalarNode yaml)
114123
{
115-
switch (yaml.Style)
124+
return yaml.Style switch
116125
{
117-
case ScalarStyle.Plain:
118-
return decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)
119-
? JsonValue.Create(d)
120-
: bool.TryParse(yaml.Value, out var b)
121-
? JsonValue.Create(b)
122-
: JsonValue.Create(yaml.Value)!;
123-
case ScalarStyle.SingleQuoted:
124-
case ScalarStyle.DoubleQuoted:
125-
case ScalarStyle.Literal:
126-
case ScalarStyle.Folded:
127-
case ScalarStyle.Any:
128-
return JsonValue.Create(yaml.Value)!;
129-
default:
130-
throw new ArgumentOutOfRangeException();
131-
}
126+
ScalarStyle.Plain when decimal.TryParse(yaml.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) => JsonValue.Create(d),
127+
ScalarStyle.Plain when bool.TryParse(yaml.Value, out var b) => JsonValue.Create(b),
128+
ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => (JsonValue)JsonNullSentinel.JsonNull.DeepClone(),
129+
ScalarStyle.Plain => JsonValue.Create(yaml.Value),
130+
ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded or ScalarStyle.Any => JsonValue.Create(yaml.Value),
131+
_ => throw new ArgumentOutOfRangeException(nameof(yaml)),
132+
};
132133
}
133134

134135
private static YamlScalarNode ToYamlScalar(this JsonValue val)
135136
{
136-
return new YamlScalarNode(val.ToJsonString());
137+
// Try to get the underlying value based on its actual type
138+
// First try to get it as a string
139+
if (val.GetValueKind() == JsonValueKind.String &&
140+
val.TryGetValue(out string? stringValue))
141+
{
142+
// For string values, we need to determine if they should be quoted in YAML
143+
// Strings that look like numbers, booleans, or null need to be quoted
144+
// to preserve their string type when round-tripping
145+
var needsQuoting = decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out _) ||
146+
bool.TryParse(stringValue, out _) ||
147+
YamlNullRepresentations.Contains(stringValue);
148+
149+
return new YamlScalarNode(stringValue)
150+
{
151+
Style = needsQuoting ? ScalarStyle.DoubleQuoted : ScalarStyle.Plain
152+
};
153+
}
154+
155+
// For non-string values (numbers, booleans, null), use their string representation
156+
// These should remain unquoted in YAML
157+
var valueString = val.ToString();
158+
return new YamlScalarNode(valueString)
159+
{
160+
Style = ScalarStyle.Plain
161+
};
137162
}
138163
}
139164
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
using System.Text.Json;
6+
using System.Text.Json.Nodes;
7+
8+
namespace Microsoft.OpenApi;
9+
10+
/// <summary>
11+
/// A sentinel value representing JSON null.
12+
/// This can only be used for OpenAPI properties of type <see cref="JsonNode"/>
13+
/// </summary>
14+
public static class JsonNullSentinel
15+
{
16+
private const string SentinelValue = "openapi-json-null-sentinel-value-2BF93600-0FE4-4250-987A-E5DDB203E464";
17+
private static readonly JsonValue SentinelJsonValue = JsonValue.Create(SentinelValue)!;
18+
/// <summary>
19+
/// A sentinel value representing JSON null.
20+
/// This can only be used for OpenAPI properties of type <see cref="JsonNode"/>.
21+
/// This can only be used for the root level of a JSON structure.
22+
/// Any use outside of these constraints is unsupported and may lead to unexpected behavior.
23+
/// Because the value might be cloned, so the value can be added in a tree, reference equality checks will not work.
24+
/// You must use the <see cref="IsJsonNullSentinel(JsonNode?)"/> method to check for this sentinel.
25+
/// </summary>
26+
public static JsonValue JsonNull => SentinelJsonValue;
27+
28+
/// <summary>
29+
/// Determines if the given node is the JSON null sentinel.
30+
/// </summary>
31+
/// <param name="node">The JsonNode to check.</param>
32+
/// <returns>Whether or not the given node is the JSON null sentinel.</returns>
33+
public static bool IsJsonNullSentinel(this JsonNode? node)
34+
{
35+
return node == SentinelJsonValue ||
36+
node is not null &&
37+
node.GetValueKind() == JsonValueKind.String &&
38+
// using deep equals here results in fewer allocations than TryGetValue
39+
JsonNode.DeepEquals(SentinelJsonValue, node);
40+
}
41+
}

src/Microsoft.OpenApi/Models/Interfaces/IOpenApiExample.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public interface IOpenApiExample : IOpenApiDescribedElement, IOpenApiSummarizedE
1212
/// Embedded literal example. The value field and externalValue field are mutually
1313
/// exclusive. To represent examples of media types that cannot naturally represented
1414
/// in JSON or YAML, use a string value to contain the example, escaping where necessary.
15+
/// You must use the <see cref="JsonNullSentinel.IsJsonNullSentinel(JsonNode?)"/> method to check whether Default was assigned a null value in the document.
16+
/// Assign <see cref="JsonNullSentinel.JsonNull"/> to use get null as a serialized value.
1517
/// </summary>
1618
public JsonNode? Value { get; }
1719

0 commit comments

Comments
 (0)