Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/release-please-gha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ on:
- support/v1
- support/v2

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.3.7"
".": "2.3.8"
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [2.3.8](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.7...v2.3.8) (2025-10-27)


### Bug Fixes

* an issue where numeric property names would be missing quotes in yaml conversion ([da43c98](https://github.com/microsoft/OpenAPI.NET/commit/da43c98cfd7938a2354bfe57f431aa4bd0407b66))
* an issue where numeric property names would be missing quotes in yaml conversion ([234504c](https://github.com/microsoft/OpenAPI.NET/commit/234504c6a1a53be8630b1f2eda8640f04a92327d))
* quote property names in yaml that match boolean values ([39a9f41](https://github.com/microsoft/OpenAPI.NET/commit/39a9f4112a123b9207504d4a840a9be553703555))
* yaml blocks and line returns ([b053848](https://github.com/microsoft/OpenAPI.NET/commit/b05384872e9364aedf8d8fc24b36bab9824594c5))
* yaml multi-line literals maintain their lines ([558a1ce](https://github.com/microsoft/OpenAPI.NET/commit/558a1ceafc22e6075470a8799582575c8c1e125d))

## [2.3.7](https://github.com/microsoft/OpenAPI.NET/compare/v2.3.6...v2.3.7) (2025-10-24)


Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageProjectUrl>https://github.com/Microsoft/OpenAPI.NET</PackageProjectUrl>
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
<PackageTags>OpenAPI .NET</PackageTags>
<Version>2.3.7</Version>
<Version>2.3.8</Version>
</PropertyGroup>
<!-- https://github.com/clairernovotny/DeterministicBuilds#deterministic-builds -->
<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>

<PackageReference Include="SharpYaml" Version="2.1.3" />
<PackageReference Include="SharpYaml" Version="2.1.4" />
<PackageReference Include="System.Text.Json" Version="[8.0.5,)" />
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-hh2w-p6rv-4g7w" />
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-8g4q-xg66-9fp4" />
Expand Down
25 changes: 20 additions & 5 deletions src/Microsoft.OpenApi.YamlReader/YamlConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ public static JsonObject ToJsonObject(this YamlMappingNode yaml)

private static YamlMappingNode ToYamlMapping(this JsonObject obj)
{
return new YamlMappingNode(obj.ToDictionary(x => (YamlNode)new YamlScalarNode(x.Key), x => x.Value!.ToYamlNode()));
return new YamlMappingNode(obj.ToDictionary(x => (YamlNode)new YamlScalarNode(x.Key)
{
Style = NeedsQuoting(x.Key) ? ScalarStyle.DoubleQuoted : ScalarStyle.Plain
}, x => x.Value!.ToYamlNode()));
}

/// <summary>
Expand Down Expand Up @@ -132,6 +135,11 @@ ScalarStyle.Plain when YamlNullRepresentations.Contains(yaml.Value) => (JsonValu
};
}

private static bool NeedsQuoting(string value) =>
decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out _) ||
bool.TryParse(value, out _) ||
YamlNullRepresentations.Contains(value);

private static YamlScalarNode ToYamlScalar(this JsonValue val)
{
// Try to get the underlying value based on its actual type
Expand All @@ -142,13 +150,20 @@ private static YamlScalarNode ToYamlScalar(this JsonValue val)
// 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);
var needsQuoting = NeedsQuoting(stringValue);

var containsNewLine = stringValue.Contains('\n');

var style = (needsQuoting, containsNewLine) switch
{
(true, _) => ScalarStyle.DoubleQuoted,
(false, true) => ScalarStyle.Literal,
(false, false) => ScalarStyle.Plain
};

return new YamlScalarNode(stringValue)
{
Style = needsQuoting ? ScalarStyle.DoubleQuoted : ScalarStyle.Plain
Style = style
};
}

Expand Down
103 changes: 97 additions & 6 deletions test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.OpenApi.YamlReader;
using Microsoft.OpenApi.Tests;
using Microsoft.OpenApi.YamlReader;
using SharpYaml;
using SharpYaml.Serialization;
using System.IO;
Expand Down Expand Up @@ -208,10 +209,7 @@ public void ToYamlNode_StringWithLineBreaks_PreservesLineBreaks()
var yamlOutput = ConvertYamlNodeToString(yamlNode);

// Convert back to JSON to verify round-tripping
var yamlStream = new YamlStream();
using var sr = new StringReader(yamlOutput);
yamlStream.Load(sr);
var jsonBack = yamlStream.Documents[0].ToJsonNode();
var jsonBack = ConvertYamlStringToJsonNode(yamlOutput);

// Assert - line breaks should be preserved during round-trip
var originalMultiline = json["multiline"]?.GetValue<string>();
Expand All @@ -225,12 +223,105 @@ public void ToYamlNode_StringWithLineBreaks_PreservesLineBreaks()
Assert.Contains("\n", roundTripDescription);
}

[Fact]
public void NumericPropertyNamesShouldRemainStringsFromJson()
{
// Given
var yamlInput =
"""
"123": value1
"456": value2
""";

// Given
var jsonNode = Assert.IsType<JsonObject>(JsonNode.Parse(@"{
""123"": ""value1"",
""456"": ""value2""
}"));

// When
var convertedBack = jsonNode.ToYamlNode();
var convertedBackOutput = ConvertYamlNodeToString(convertedBack);

// Then
Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral());
}

[Fact]
public void NumericPropertyNamesShouldRemainStringsFromYaml()
{
// Given
var yamlInput =
"""
"123": value1
"456": value2
""";

var jsonNode = ConvertYamlStringToJsonNode(yamlInput);

var convertedBack = jsonNode.ToYamlNode();
var convertedBackOutput = ConvertYamlNodeToString(convertedBack);
// Then
Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral());
}

[Fact]
public void BooleanPropertyNamesShouldRemainStringsFromYaml()
{
// Given
var yamlInput =
"""
"true": value1
"false": value2
""";

var jsonNode = ConvertYamlStringToJsonNode(yamlInput);

var convertedBack = jsonNode.ToYamlNode();
var convertedBackOutput = ConvertYamlNodeToString(convertedBack);
// Then
Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral());
}

[Fact]
public void LineBreaksShouldRoundTrip()
{
var yamlInput =
"""
python: |-
from openai import OpenAI
client = OpenAI(
api_key="My API Key",
)
page = client.beta.assistants.list()
page = page.data[0]
print(page.id)
""";
// When
var jsonNode = ConvertYamlStringToJsonNode(yamlInput);
var convertedBack = jsonNode.ToYamlNode();
var convertedBackOutput = ConvertYamlNodeToString(convertedBack);

// Then
Assert.Equal(yamlInput.MakeLineBreaksEnvironmentNeutral(), convertedBackOutput.MakeLineBreaksEnvironmentNeutral());
}

private static JsonNode ConvertYamlStringToJsonNode(string yamlInput)
{
var yamlDocument = new YamlStream();
using var sr = new StringReader(yamlInput);
yamlDocument.Load(sr);
var yamlRoot = yamlDocument.Documents[0].RootNode;
return yamlRoot.ToJsonNode();
}

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);
yamlStream.Save(writer, isLastDocumentEndImplicit: true);
writer.Flush();
ms.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(ms);
Expand Down