Skip to content

Commit

Permalink
Merge pull request #2112 from microsoft/fix/nullable-down-cast-3-0
Browse files Browse the repository at this point in the history
fix: a bug where 3.0 downcast of type null would not work
  • Loading branch information
baywet authored Feb 4, 2025
2 parents 1043e4e + 974ab44 commit 6b636d5
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 30 deletions.
68 changes: 47 additions & 21 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand All @@ -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()));
}
}
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"maximum": 42,
"minimum": 10,
"exclusiveMinimum": true,
"nullable": true,
"type": "integer",
"default": 15,
"nullable": true,
"externalDocs": {
"url": "http://example.com/externalDocs"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","default":15,"nullable":true,"externalDocs":{"url":"http://example.com/externalDocs"}}
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"nullable":true,"type":"integer","default":15,"externalDocs":{"url":"http://example.com/externalDocs"}}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"maximum": 42,
"minimum": 10,
"exclusiveMinimum": true,
"nullable": true,
"type": "integer",
"default": 15,
"nullable": true,
"externalDocs": {
"url": "http://example.com/externalDocs"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"type":"integer","default":15,"nullable":true,"externalDocs":{"url":"http://example.com/externalDocs"}}
{"title":"title1","multipleOf":3,"maximum":42,"minimum":10,"exclusiveMinimum":true,"nullable":true,"type":"integer","default":15,"externalDocs":{"url":"http://example.com/externalDocs"}}
10 changes: 5 additions & 5 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -268,6 +268,7 @@ public async Task SerializeAdvancedSchemaObjectAsV3JsonWorks()
"""
{
"title": "title1",
"nullable": true,
"properties": {
"property1": {
"properties": {
Expand Down Expand Up @@ -296,7 +297,6 @@ public async Task SerializeAdvancedSchemaObjectAsV3JsonWorks()
}
}
},
"nullable": true,
"externalDocs": {
"url": "http://example.com/externalDocs"
}
Expand All @@ -320,6 +320,7 @@ public async Task SerializeAdvancedSchemaWithAllOfAsV3JsonWorks()
"""
{
"title": "title1",
"nullable": true,
"allOf": [
{
"title": "title2",
Expand All @@ -335,6 +336,7 @@ public async Task SerializeAdvancedSchemaWithAllOfAsV3JsonWorks()
},
{
"title": "title3",
"nullable": true,
"properties": {
"property3": {
"properties": {
Expand All @@ -347,11 +349,9 @@ public async Task SerializeAdvancedSchemaWithAllOfAsV3JsonWorks()
"minLength": 2,
"type": "string"
}
},
"nullable": true
}
}
],
"nullable": true,
"externalDocs": {
"url": "http://example.com/externalDocs"
}
Expand Down

0 comments on commit 6b636d5

Please # to comment.