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/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() { 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); + } +}