Skip to content

Commit f1c9297

Browse files
Merge pull request #1820 from microsoft/mk/handle-schemas-with-multiple-types
Handle upcasting and downcasting of JSON schemas with type arrays
2 parents 4fdd0e8 + 6ac0bd1 commit f1c9297

File tree

8 files changed

+234
-13
lines changed

8 files changed

+234
-13
lines changed

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,16 @@ public static class OpenApiConstants
700700
/// </summary>
701701
public const string ComponentsSegment = "/components/";
702702

703+
/// <summary>
704+
/// Field: Null
705+
/// </summary>
706+
public const string Null = "null";
707+
708+
/// <summary>
709+
/// Field: Nullable extension
710+
/// </summary>
711+
public const string NullableExtension = "x-nullable";
712+
703713
#region V2.0
704714

705715
/// <summary>

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -483,14 +483,7 @@ public void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
483483
writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (nodeWriter, s) => nodeWriter.WriteAny(s));
484484

485485
// type
486-
if (Type?.GetType() == typeof(string))
487-
{
488-
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
489-
}
490-
else
491-
{
492-
writer.WriteOptionalCollection(OpenApiConstants.Type, (string[])Type, (w, s) => w.WriteRaw(s));
493-
}
486+
SerializeTypeProperty(Type, writer, version);
494487

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

535528
// nullable
536-
writer.WriteProperty(OpenApiConstants.Nullable, Nullable, false);
529+
if (version is OpenApiSpecVersion.OpenApi3_0)
530+
{
531+
writer.WriteProperty(OpenApiConstants.Nullable, Nullable, false);
532+
}
537533

538534
// discriminator
539535
writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, (w, s) => s.SerializeAsV3(w));
@@ -670,7 +666,14 @@ internal void SerializeAsV2(
670666
writer.WriteStartObject();
671667

672668
// type
673-
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
669+
if (Type is string[] array)
670+
{
671+
DowncastTypeArrayToV2OrV3(array, writer, OpenApiSpecVersion.OpenApi2_0);
672+
}
673+
else
674+
{
675+
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
676+
}
674677

675678
// description
676679
writer.WriteProperty(OpenApiConstants.Description, Description);
@@ -799,6 +802,35 @@ internal void SerializeAsV2(
799802
writer.WriteEndObject();
800803
}
801804

805+
private void SerializeTypeProperty(object type, IOpenApiWriter writer, OpenApiSpecVersion version)
806+
{
807+
if (type?.GetType() == typeof(string))
808+
{
809+
// check whether nullable is true for upcasting purposes
810+
if (Nullable || Extensions.ContainsKey(OpenApiConstants.NullableExtension))
811+
{
812+
// create a new array and insert the type and "null" as values
813+
Type = new[] { (string)Type, OpenApiConstants.Null };
814+
}
815+
else
816+
{
817+
writer.WriteProperty(OpenApiConstants.Type, (string)Type);
818+
}
819+
}
820+
if (Type is string[] array)
821+
{
822+
// type
823+
if (version is OpenApiSpecVersion.OpenApi3_0)
824+
{
825+
DowncastTypeArrayToV2OrV3(array, writer, OpenApiSpecVersion.OpenApi3_0);
826+
}
827+
else
828+
{
829+
writer.WriteOptionalCollection(OpenApiConstants.Type, (string[])Type, (w, s) => w.WriteRaw(s));
830+
}
831+
}
832+
}
833+
802834
private object DeepCloneType(object type)
803835
{
804836
if (type == null)
@@ -822,5 +854,38 @@ private object DeepCloneType(object type)
822854

823855
return null;
824856
}
857+
858+
private void DowncastTypeArrayToV2OrV3(string[] array, IOpenApiWriter writer, OpenApiSpecVersion version)
859+
{
860+
/* If the array has one non-null value, emit Type as string
861+
* If the array has one null value, emit x-nullable as true
862+
* If the array has two values, one null and one non-null, emit Type as string and x-nullable as true
863+
* If the array has more than two values or two non-null values, do not emit type
864+
* */
865+
866+
var nullableProp = version.Equals(OpenApiSpecVersion.OpenApi2_0)
867+
? OpenApiConstants.NullableExtension
868+
: OpenApiConstants.Nullable;
869+
870+
if (array.Length is 1)
871+
{
872+
var value = array[0];
873+
if (value is OpenApiConstants.Null)
874+
{
875+
writer.WriteProperty(nullableProp, true);
876+
}
877+
else
878+
{
879+
writer.WriteProperty(OpenApiConstants.Type, value);
880+
}
881+
}
882+
else if (array.Length is 2 && array.Contains(OpenApiConstants.Null))
883+
{
884+
// Find the non-null value and write it out
885+
var nonNullValue = array.First(v => v != OpenApiConstants.Null);
886+
writer.WriteProperty(OpenApiConstants.Type, nonNullValue);
887+
writer.WriteProperty(nullableProp, true);
888+
}
889+
}
825890
}
826891
}

src/Microsoft.OpenApi/Reader/V31/OpenApiSchemaDeserializer.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,22 @@ internal static partial class OpenApiV31Deserializer
182182
},
183183
{
184184
"nullable",
185-
(o, n, _) => o.Nullable = bool.Parse(n.GetScalarValue())
185+
(o, n, _) =>
186+
{
187+
var nullable = bool.Parse(n.GetScalarValue());
188+
if (nullable) // if nullable, convert type into an array of type(s) and null
189+
{
190+
if (o.Type is string[] typeArray)
191+
{
192+
var typeList = new List<string>(typeArray) { OpenApiConstants.Null };
193+
o.Type = typeList.ToArray();
194+
}
195+
else if (o.Type is string typeString)
196+
{
197+
o.Type = new string[]{typeString, OpenApiConstants.Null};
198+
}
199+
}
200+
}
186201
},
187202
{
188203
"discriminator",
@@ -242,6 +257,13 @@ public static OpenApiSchema LoadSchema(ParseNode node, OpenApiDocument hostDocum
242257
propertyNode.ParseField(schema, _openApiSchemaFixedFields, _openApiSchemaPatternFields);
243258
}
244259

260+
if (schema.Extensions.ContainsKey(OpenApiConstants.NullableExtension))
261+
{
262+
var type = schema.Type;
263+
schema.Type = new string[] {(string)type, OpenApiConstants.Null};
264+
schema.Extensions.Remove(OpenApiConstants.NullableExtension);
265+
}
266+
245267
return schema;
246268
}
247269
}

test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiSchemaTests.cs

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

44
using System.Collections.Generic;
5+
using System.IO;
56
using System.Text.Json.Nodes;
67
using FluentAssertions;
78
using FluentAssertions.Equivalency;
8-
using Microsoft.OpenApi.Any;
99
using Microsoft.OpenApi.Models;
1010
using Microsoft.OpenApi.Reader;
11+
using Microsoft.OpenApi.Tests;
12+
using Microsoft.OpenApi.Writers;
1113
using Xunit;
1214

1315
namespace Microsoft.OpenApi.Readers.Tests.V31Tests
@@ -289,5 +291,118 @@ public void CloningSchemaWithExamplesAndEnumsShouldSucceed()
289291
clone.Examples.Should().NotBeEquivalentTo(schema.Examples);
290292
clone.Default.Should().NotBeEquivalentTo(schema.Default);
291293
}
294+
295+
[Fact]
296+
public void SerializeV31SchemaWithMultipleTypesAsV3Works()
297+
{
298+
// Arrange
299+
var expected = @"type: string
300+
nullable: true";
301+
302+
var path = Path.Combine(SampleFolderPath, "schemaWithTypeArray.yaml");
303+
304+
// Act
305+
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);
306+
307+
var writer = new StringWriter();
308+
schema.SerializeAsV3(new OpenApiYamlWriter(writer));
309+
var schema1String = writer.ToString();
310+
311+
schema1String.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
312+
}
313+
314+
[Fact]
315+
public void SerializeV31SchemaWithMultipleTypesAsV2Works()
316+
{
317+
// Arrange
318+
var expected = @"type: string
319+
x-nullable: true";
320+
321+
var path = Path.Combine(SampleFolderPath, "schemaWithTypeArray.yaml");
322+
323+
// Act
324+
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);
325+
326+
var writer = new StringWriter();
327+
schema.SerializeAsV2(new OpenApiYamlWriter(writer));
328+
var schema1String = writer.ToString();
329+
330+
schema1String.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
331+
}
332+
333+
[Fact]
334+
public void SerializeV3SchemaWithNullableAsV31Works()
335+
{
336+
// Arrange
337+
var expected = @"type:
338+
- string
339+
- null";
340+
341+
var path = Path.Combine(SampleFolderPath, "schemaWithNullable.yaml");
342+
343+
// Act
344+
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_0, out _);
345+
346+
var writer = new StringWriter();
347+
schema.SerializeAsV31(new OpenApiYamlWriter(writer));
348+
var schemaString = writer.ToString();
349+
350+
schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
351+
}
352+
353+
[Fact]
354+
public void SerializeV2SchemaWithNullableExtensionAsV31Works()
355+
{
356+
// Arrange
357+
var expected = @"type:
358+
- string
359+
- null
360+
x-nullable: true";
361+
362+
var path = Path.Combine(SampleFolderPath, "schemaWithNullableExtension.yaml");
363+
364+
// Act
365+
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi2_0, out _);
366+
367+
var writer = new StringWriter();
368+
schema.SerializeAsV31(new OpenApiYamlWriter(writer));
369+
var schemaString = writer.ToString();
370+
371+
schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
372+
}
373+
374+
[Fact]
375+
public void SerializeSchemaWithTypeArrayAndNullableDoesntEmitType()
376+
{
377+
var input = @"type:
378+
- ""string""
379+
- ""int""
380+
nullable: true";
381+
382+
var expected = @"{ }";
383+
384+
var schema = OpenApiModelFactory.Parse<OpenApiSchema>(input, OpenApiSpecVersion.OpenApi3_1, out _, "yaml");
385+
386+
var writer = new StringWriter();
387+
schema.SerializeAsV2(new OpenApiYamlWriter(writer));
388+
var schemaString = writer.ToString();
389+
390+
schemaString.MakeLineBreaksEnvironmentNeutral().Should().Be(expected.MakeLineBreaksEnvironmentNeutral());
391+
}
392+
393+
[Theory]
394+
[InlineData("schemaWithNullable.yaml")]
395+
[InlineData("schemaWithNullableExtension.yaml")]
396+
public void LoadSchemaWithNullableExtensionAsV31Works(string filePath)
397+
{
398+
// Arrange
399+
var path = Path.Combine(SampleFolderPath, filePath);
400+
401+
// Act
402+
var schema = OpenApiModelFactory.Load<OpenApiSchema>(path, OpenApiSpecVersion.OpenApi3_1, out _);
403+
404+
// Assert
405+
schema.Type.Should().BeEquivalentTo(new string[] { "string", "null" });
406+
}
292407
}
293408
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
type: string
2+
nullable: true
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
type: string
2+
x-nullable: true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type:
2+
- "string"
3+
- "null"

test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,9 @@ namespace Microsoft.OpenApi.Models
437437
public const string Name = "name";
438438
public const string Namespace = "namespace";
439439
public const string Not = "not";
440+
public const string Null = "null";
440441
public const string Nullable = "nullable";
442+
public const string NullableExtension = "x-nullable";
441443
public const string OneOf = "oneOf";
442444
public const string OpenApi = "openapi";
443445
public const string OpenIdConnectUrl = "openIdConnectUrl";

0 commit comments

Comments
 (0)