From ac05342befbe51944cf3a1c966d564077e8e28ea Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 31 Jan 2025 14:24:29 -0500 Subject: [PATCH 1/7] fix: a bug where 3.0 downcast of type null would not work --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 49 ++++++++++++------- ...sync_produceTerseOutput=False.verified.txt | 2 +- ...Async_produceTerseOutput=True.verified.txt | 2 +- ...sync_produceTerseOutput=False.verified.txt | 2 +- ...Async_produceTerseOutput=True.verified.txt | 2 +- .../Models/OpenApiSchemaTests.cs | 10 ++-- 6 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index aae5723e8..14ab1001b 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; @@ -355,12 +357,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); @@ -635,20 +631,33 @@ private void SerializeAsV2( private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version) { + // check whether nullable is true for upcasting purposes + var isNullable = Nullable || + Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && + nullExtRawValue is OpenApiAny openApiAny && + openApiAny.Node is 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: + writer.WriteProperty(OpenApiConstants.Nullable, true); + goto default; + default: + writer.WriteProperty(OpenApiConstants.Type, type.Value.ToIdentifier()); + break; } } else @@ -663,6 +672,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())); } } @@ -680,12 +693,12 @@ private static bool HasMultipleTypes(JsonSchemaType schemaType) schemaTypeNumeric != (int)JsonSchemaType.Null; } - private void UpCastSchemaTypeToV31(JsonSchemaType? type, IOpenApiWriter writer) + private void UpCastSchemaTypeToV31(JsonSchemaType type, IOpenApiWriter writer) { // create a new array and insert the type and "null" as values - Type = type | JsonSchemaType.Null; + 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 Type.Value.HasFlag(flag) + where temporaryType.HasFlag(flag) select flag.ToIdentifier()).ToList(); writer.WriteOptionalCollection(OpenApiConstants.Type, list, (w, s) => w.WriteValue(s)); } 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 ffb10aa38..898a96627 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" } From a5023d659b7adaedbe18853c297e78ac12e22823 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 31 Jan 2025 14:31:01 -0500 Subject: [PATCH 2/7] fix: null reference check Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 14ab1001b..c6d3219af 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -633,6 +633,7 @@ private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, { // check whether nullable is true for upcasting purposes var isNullable = Nullable || + Extensions is not null && Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && nullExtRawValue is OpenApiAny openApiAny && openApiAny.Node is JsonNode jsonNode && From 121bb48a8f376e0257e4c2ced80978116efaa3a3 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 31 Jan 2025 14:35:09 -0500 Subject: [PATCH 3/7] chore: formatting --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index c6d3219af..319734dc6 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -635,8 +635,7 @@ private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, var isNullable = Nullable || Extensions is not null && Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && - nullExtRawValue is OpenApiAny openApiAny && - openApiAny.Node is JsonNode jsonNode && + nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode} && jsonNode.GetValueKind() is JsonValueKind.True; if (type is null) { From 920a51a9170eb76921fd0e6529461e7681ac4c19 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 31 Jan 2025 15:00:14 -0500 Subject: [PATCH 4/7] fix: 3.0 serialization when type is set to null Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 319734dc6..355172035 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -632,7 +632,8 @@ private void SerializeAsV2( private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, OpenApiSpecVersion version) { // check whether nullable is true for upcasting purposes - var isNullable = Nullable || + var isNullable = Nullable || + Type is JsonSchemaType.Null || Extensions is not null && Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode} && @@ -652,9 +653,14 @@ Extensions is not null && case OpenApiSpecVersion.OpenApi3_1 when isNullable: UpCastSchemaTypeToV31(type.Value, writer); break; - case OpenApiSpecVersion.OpenApi3_0 when isNullable: + case OpenApiSpecVersion.OpenApi3_0 when isNullable && type.Value == JsonSchemaType.Null: writer.WriteProperty(OpenApiConstants.Nullable, true); - goto default; + 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; From 3b3d0e6da51f7958285b8aa5be5d1eb73ec69acd Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 31 Jan 2025 15:29:25 -0500 Subject: [PATCH 5/7] fix: do not emit a type array in 3.1 when unnecessary Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 355172035..0296a1d5f 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -706,7 +706,14 @@ private void UpCastSchemaTypeToV31(JsonSchemaType type, IOpenApiWriter writer) 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 From 081e2511b9df964ad74f7cb0e48761977e50cc45 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Feb 2025 08:55:20 -0500 Subject: [PATCH 6/7] fix: null flag comparison Co-authored-by: Andrew Omondi --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 0296a1d5f..c664aec3d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -633,7 +633,7 @@ private void SerializeTypeProperty(JsonSchemaType? type, IOpenApiWriter writer, { // check whether nullable is true for upcasting purposes var isNullable = Nullable || - Type is JsonSchemaType.Null || + (Type.HasValue && Type.Value.HasFlag(JsonSchemaType.Null)) || Extensions is not null && Extensions.TryGetValue(OpenApiConstants.NullableExtension, out var nullExtRawValue) && nullExtRawValue is OpenApiAny { Node: JsonNode jsonNode} && From 306cd32cd9727e7b17a4117c84f39bb661a90e88 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 3 Feb 2025 09:20:28 -0500 Subject: [PATCH 7/7] chore: code linting Signed-off-by: Vincent Biret --- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index c664aec3d..2cf729e0f 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs @@ -699,11 +699,11 @@ 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 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 + 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(); if (list.Count > 1) @@ -736,7 +736,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)