diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 482ee6b3c..b8dfcefd4 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Text.Json.Nodes; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Helpers; using Microsoft.OpenApi.Interfaces; @@ -356,12 +358,6 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version // default writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d)); - // nullable - if (version is OpenApiSpecVersion.OpenApi3_0) - { - writer.WriteProperty(OpenApiConstants.Nullable, Nullable, false); - } - // discriminator writer.WriteOptionalObject(OpenApiConstants.Discriminator, Discriminator, callback); @@ -636,20 +632,39 @@ private void SerializeAsV2( private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version) { + // check whether nullable is true for upcasting purposes + var isNullable = Nullable || + (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) || + Extensions is not null && + Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && + nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode} && + jsonNode.GetValueKind() is JsonValueKind.True; if (type is null) { - return; - } - if (!HasMultipleTypes(type.Value)) - { - // check whether nullable is true for upcasting purposes - if (version is OpenApiSpecVersion.OpenApi3_1 && (Nullable || Extensions.ContainsKey(OpenApiConstants.NullableExtension))) + if (version is OpenApiSpecVersion.OpenApi3_0 && isNullable) { - UpCastSchemaTypeToV31(type, writer); + writer.WriteProperty(OpenApiConstants.Nullable, true); } - else + } + else if (!HasMultipleTypes(type.Value)) + { + + switch (version) { - writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier()); + case OpenApiSpecVersion.OpenApi3_1 when isNullable: + UpCastSchemaTypeToV31(type.Value, writer); + break; + case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value == JsonSchemaType.Null: + writer.WriteProperty(OpenApiConstants.Nullable, true); + writer.WriteProperty(OpenApiConstants.Type, JsonSchemaType.Object.ToIdentifier()); + break; + case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value != JsonSchemaType.Null: + writer.WriteProperty(OpenApiConstants.Nullable, true); + writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier()); + break; + default: + writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier()); + break; } } else @@ -664,6 +679,10 @@ private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, var list = (from JsonSchemaType flag in jsonSchemaTypeValues where type.Value.HasFlag(flag) select flag).ToList(); + if (Nullable && !list.Contains(JsonSchemaType.Null)) + { + list.Add(JsonSchemaType.Null); + } writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s.ToIdentifier())); } } @@ -681,14 +700,21 @@ private static bool HasMultipleTypes(JsonSchemaType schemaType) schemaTypeNumeric != (int)JsonSchemaType.Null; } - private void UpCastSchemaTypeToV31(JsonSchemaType? type, IOpenApiWriter writer) + private static void UpCastSchemaTypeToV31(JsonSchemaType type, IOpenApiWriter writer) { // create a new array and insert the type and "null" as values - Type = type | JsonSchemaType.Null; - var list = (from JsonSchemaType? flag in jsonSchemaTypeValues// Check if the flag is set in 'type' using a bitwise AND operation - where Type.Value.HasFlag(flag) + var temporaryType = type | JsonSchemaType.Null; + var list = (from JsonSchemaType flag in jsonSchemaTypeValues// Check if the flag is set in 'type' using a bitwise AND operation + where temporaryType.HasFlag(flag) select flag.ToIdentifier()).ToList(); - writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s)); + if (list.Count > 1) + { + writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s)); + } + else + { + writer.WriteProperty(OpenApiConstants.Type, list[0]); + } } #if NET5_0_OR_GREATER @@ -711,7 +737,7 @@ private void DowncastTypeArrayToV2OrV3(JsonSchemaType schemaType, IOpenApiWriter if (!HasMultipleTypes(schemaType ^ JsonSchemaType.Null) && (schemaType & JsonSchemaType.Null) == JsonSchemaType.Null) // checks for two values and one is null { - foreach (JsonSchemaType? flag in jsonSchemaTypeValues) + foreach (JsonSchemaType flag in jsonSchemaTypeValues) { // Skip if the flag is not set or if it's the Null flag if (schemaType.HasFlag(flag) && flag != JsonSchemaType.Null) diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=False.verified.txt index b431f1607..852e12e71 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=False.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=False.verified.txt @@ -4,9 +4,9 @@ "maximum": 42, "minimum": 10, "exclusiveMinimum": true, + "nullable": true, "type": "integer", "default": 15, - "nullable": true, "externalDocs": { "url": "http://example.com/externalDocs" } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=True.verified.txt index d71a5f0a8..bfea35bdd 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=True.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3JsonWorksAsync_produceTerseOutput=True.verified.txt @@ -1 +1 @@ -{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","default":15,"nullable":true,"externalDocs":{"url":"http://example.com/externalDocs"}} \ No newline at end of file +{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"nullable":true,"type":"integer","default":15,"externalDocs":{"url":"http://example.com/externalDocs"}} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=False.verified.txt index b431f1607..852e12e71 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=False.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=False.verified.txt @@ -4,9 +4,9 @@ "maximum": 42, "minimum": 10, "exclusiveMinimum": true, + "nullable": true, "type": "integer", "default": 15, - "nullable": true, "externalDocs": { "url": "http://example.com/externalDocs" } diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=True.verified.txt index d71a5f0a8..bfea35bdd 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=True.verified.txt +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.SerializeReferencedSchemaAsV3WithoutReferenceJsonWorksAsync_produceTerseOutput=True.verified.txt @@ -1 +1 @@ -{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","default":15,"nullable":true,"externalDocs":{"url":"http://example.com/externalDocs"}} \ No newline at end of file +{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"nullable":true,"type":"integer","default":15,"externalDocs":{"url":"http://example.com/externalDocs"}} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index c035b04ce..bc3a78a7c 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -242,9 +242,9 @@ public async Task SerializeAdvancedSchemaNumberAsV3JsonWorks() "maximum": 42, "minimum": 10, "exclusiveMinimum": true, + "nullable": true, "type": "integer", "default": 15, - "nullable": true, "externalDocs": { "url": "http://example.com/externalDocs" } @@ -268,6 +268,7 @@ public async Task SerializeAdvancedSchemaObjectAsV3JsonWorks() """ { "title": "title1", + "nullable": true, "properties": { "property1": { "properties": { @@ -296,7 +297,6 @@ public async Task SerializeAdvancedSchemaObjectAsV3JsonWorks() } } }, - "nullable": true, "externalDocs": { "url": "http://example.com/externalDocs" } @@ -320,6 +320,7 @@ public async Task SerializeAdvancedSchemaWithAllOfAsV3JsonWorks() """ { "title": "title1", + "nullable": true, "allOf": [ { "title": "title2", @@ -335,6 +336,7 @@ public async Task SerializeAdvancedSchemaWithAllOfAsV3JsonWorks() }, { "title": "title3", + "nullable": true, "properties": { "property3": { "properties": { @@ -347,11 +349,9 @@ public async Task SerializeAdvancedSchemaWithAllOfAsV3JsonWorks() "minLength": 2, "type": "string" } - }, - "nullable": true + } } ], - "nullable": true, "externalDocs": { "url": "http://example.com/externalDocs" }