diff --git a/.github/workflows/release-please-gha.yml b/.github/workflows/release-please-gha.yml
index 5750600d8..9c76a0d64 100644
--- a/.github/workflows/release-please-gha.yml
+++ b/.github/workflows/release-please-gha.yml
@@ -17,6 +17,9 @@ on:
- support/v1
- support/v2
+permissions:
+ contents: read
+
jobs:
release:
runs-on: ubuntu-latest
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2d6304e18..4d2b02b5e 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "2.3.7"
+ ".": "2.3.8"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fd74b4886..b8d1854c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/Directory.Build.props b/Directory.Build.props
index 17a280e02..ea4943006 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -12,7 +12,7 @@
https://github.com/Microsoft/OpenAPI.NET
© Microsoft Corporation. All rights reserved.
OpenAPI .NET
- 2.3.7
+ 2.3.8
diff --git a/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj b/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj
index c836548aa..1f2f5fb36 100644
--- a/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj
+++ b/src/Microsoft.OpenApi.YamlReader/Microsoft.OpenApi.YamlReader.csproj
@@ -33,7 +33,7 @@
all
-
+
diff --git a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs
index 260e4d971..5acf0c007 100644
--- a/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs
+++ b/src/Microsoft.OpenApi.YamlReader/YamlConverter.cs
@@ -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()));
}
///
@@ -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
@@ -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
};
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs
index a40febbbf..70073bfaa 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs
@@ -1,4 +1,5 @@
-using Microsoft.OpenApi.YamlReader;
+using Microsoft.OpenApi.Tests;
+using Microsoft.OpenApi.YamlReader;
using SharpYaml;
using SharpYaml.Serialization;
using System.IO;
@@ -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();
@@ -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(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);