Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
89 changes: 79 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 All @@ -557,6 +553,10 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false);

// extensions
if (Extensions.ContainsKey(OpenApiConstants.NullableExtension))
{
Extensions.Remove(OpenApiConstants.NullableExtension);
}
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0);

writer.WriteEndObject();
Expand Down Expand Up @@ -670,7 +670,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 +806,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 +858,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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// 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,83 @@ 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";

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