Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
10 changes: 10 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,16 @@ public static class OpenApiConstants
/// </summary>
public const string ComponentsSegment = "/components/";

/// <summary>
/// Field: Null
/// </summary>
public const string Null = "null";

/// <summary>
/// Field: Nullable extension
/// </summary>
public const string NullableExtension = "x-nullable";

#region V2.0

/// <summary>
Expand Down
85 changes: 75 additions & 10 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -483,14 +483,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (nodeWriter, s) => nodeWriter.WriteAny(s));

// type
if (Type?.GetType() == typeof(string))
{
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
}
else
{
writer.WriteOptionalCollection(OpenApiConstants.Type, (string[])Type, (w, s) => w.WriteRaw(s));
}
SerializeTypeProperty(Type, writer, version);

// allOf
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, (w, s) => s.SerializeAsV3(w));
Expand Down Expand Up @@ -533,7 +526,10 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));

// nullable
writer.WriteProperty(OpenApiConstants.Nullable, Nullable, false);
if (version is OpenApiSpecVersion.OpenApi3_0)
{
writer.WriteProperty(OpenApiConstants.Nullable, Nullable, false);
}

// discriminator
writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, (w, s) => s.SerializeAsV3(w));
Expand Down Expand Up @@ -670,7 +666,14 @@ internal void SerializeAsV2(
writer.WriteStartObject();

// type
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
if (Type is string[] array)
{
DowncastTypeArrayToV2OrV3(array, writer, OpenApiSpecVersion.OpenApi2_0);
}
else
{
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
}

// description
writer.WriteProperty(OpenApiConstants.Description, Description);
Expand Down Expand Up @@ -799,6 +802,35 @@ internal void SerializeAsV2(
writer.WriteEndObject();
}

private void SerializeTypeProperty(object type, IOpenApiWriter writer, OpenApiSpecVersion version)
{
if (type?.GetType() == typeof(string))
{
// check whether nullable is true for upcasting purposes
if (Nullable || Extensions.ContainsKey(OpenApiConstants.NullableExtension))
{
// create a new array and insert the type and "null" as values
Type = new[] { (string)Type, OpenApiConstants.Null };
}
else
{
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
}
}
if (Type is string[] array)
{
// type
if (version is OpenApiSpecVersion.OpenApi3_0)
{
DowncastTypeArrayToV2OrV3(array, writer, OpenApiSpecVersion.OpenApi3_0);
}
else
{
writer.WriteOptionalCollection(OpenApiConstants.Type, (string[])Type, (w, s) => w.WriteRaw(s));
}
}
}

private object DeepCloneType(object type)
{
if (type == null)
Expand All @@ -822,5 +854,38 @@ private object DeepCloneType(object type)

return null;
}

private void DowncastTypeArrayToV2OrV3(string[] array, IOpenApiWriter writer, OpenApiSpecVersion version)
{
/* If the array has one non-null value, emit Type as string
* If the array has one null value, emit x-nullable as true
* If the array has two values, one null and one non-null, emit Type as string and x-nullable as true
* If the array has more than two values or two non-null values, do not emit type
* */

var nullableProp = version.Equals(OpenApiSpecVersion.OpenApi2_0)
? OpenApiConstants.NullableExtension
: OpenApiConstants.Nullable;

if (array.Length is 1)
{
var value = array[0];
if (value is OpenApiConstants.Null)
{
writer.WriteProperty(nullableProp, true);
}
else
{
writer.WriteProperty(OpenApiConstants.Type, value);
}
}
else if (array.Length is 2 && array.Contains(OpenApiConstants.Null))
{
// Find the non-null value and write it out
var nonNullValue = array.First(v => v != OpenApiConstants.Null);
writer.WriteProperty(OpenApiConstants.Type, nonNullValue);
writer.WriteProperty(nullableProp, true);
}
}
}
}
16 changes: 15 additions & 1 deletion src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,14 @@ internal static partial class OpenApiV31Deserializer
},
{
"nullable",
(o, n, _) => o.Nullable = bool.Parse(n.GetScalarValue())
(o, n, _) =>
{
var nullable = bool.Parse(n.GetScalarValue());
if (nullable) // if nullable, convert type into an array of type and null
{
o.Type = new string[]{o.Type.ToString(), OpenApiConstants.Null};
}
}
},
{
"discriminator",
Expand Down Expand Up @@ -242,6 +249,13 @@ public static OpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocum
propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields);
}

if (schema.Extensions.ContainsKey(OpenApiConstants.NullableExtension))
{
var type = schema.Type;
schema.Type = new string[] {(string)type, OpenApiConstants.Null};
schema.Extensions.Remove(OpenApiConstants.NullableExtension);
}

return schema;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// Licensed under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Text.Json.Nodes;
using FluentAssertions;
using FluentAssertions.Equivalency;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Reader;
using Microsoft.OpenApi.Tests;
using Microsoft.OpenApi.Writers;
using Xunit;

namespace Microsoft.OpenApi.Readers.Tests.V31Tests
Expand Down Expand Up @@ -289,5 +291,99 @@ public void CloningSchemaWithExamplesAndEnumsShouldSucceed()
clone.Examples.Should().NotBeEquivalentTo(schema.Examples);
clone.Default.Should().NotBeEquivalentTo(schema.Default);
}

[Fact]
public void SerializeV31SchemaWithMultipleTypesAsV3Works()
{
// Arrange
var expected = @"type: string
nullable: true";

var path = Path.Combine(SampleFolderPath, "schemaWithTypeArray.yaml");

// Act
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);

var writer = new StringWriter();
schema.SerializeAsV3(new OpenApiYamlWriter(writer));
var schema1String = writer.ToString();

schema1String.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
}

[Fact]
public void SerializeV31SchemaWithMultipleTypesAsV2Works()
{
// Arrange
var expected = @"type: string
x-nullable: true";

var path = Path.Combine(SampleFolderPath, "schemaWithTypeArray.yaml");

// Act
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);

var writer = new StringWriter();
schema.SerializeAsV2(new OpenApiYamlWriter(writer));
var schema1String = writer.ToString();

schema1String.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
}

[Fact]
public void SerializeV3SchemaWithNullableAsV31Works()
{
// Arrange
var expected = @"type:
- string
- null";

var path = Path.Combine(SampleFolderPath, "schemaWithNullable.yaml");

// Act
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_0, out _);

var writer = new StringWriter();
schema.SerializeAsV31(new OpenApiYamlWriter(writer));
var schemaString = writer.ToString();

schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
}

[Fact]
public void SerializeV2SchemaWithNullableExtensionAsV31Works()
{
// Arrange
var expected = @"type:
- string
- null
x-nullable: true";

var path = Path.Combine(SampleFolderPath, "schemaWithNullableExtension.yaml");

// Act
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi2_0, out _);

var writer = new StringWriter();
schema.SerializeAsV31(new OpenApiYamlWriter(writer));
var schemaString = writer.ToString();

schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
}

[Theory]
[InlineData("schemaWithNullable.yaml")]
[InlineData("schemaWithNullableExtension.yaml")]
public void LoadSchemaWithNullableExtensionAsV31Works(string filePath)
{
// Arrange
var path = Path.Combine(SampleFolderPath, filePath);

// Act
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);

// Assert
schema.Type.Should().BeEquivalentTo(new string[] { "string", "null" });
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type: string
nullable: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type: string
x-nullable: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type:
- "string"
- "null"
2 changes: 2 additions & 0 deletions test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,9 @@ namespace Microsoft.OpenApi.Models
public const string Name = "name";
public const string Namespace = "namespace";
public const string Not = "not";
public const string Null = "null";
public const string Nullable = "nullable";
public const string NullableExtension = "x-nullable";
public const string OneOf = "oneOf";
public const string OpenApi = "openapi";
public const string OpenIdConnectUrl = "openIdConnectUrl";
Expand Down