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 1/6] 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 2/6] 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 3/6] 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 9f2bdc255e2de988873201de3fe2b4cf992198c4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 10:28:29 -0400 Subject: [PATCH 4/6] 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 5/6] 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 ab5f73bc18e6f929f8554346870308ea89eac184 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 23 Oct 2025 11:07:30 -0400 Subject: [PATCH 6/6] 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