Skip to content

Commit f4778e0

Browse files
Youssef1313baywet
authored andcommitted
fix: handling of nullable enums for 3.0 (#2920)
* Fix handling of nullable enums for 3.0 * Address comments * Add * Use JsonNullSentinel.JsonNull
1 parent b8c1e27 commit f4778e0

2 files changed

Lines changed: 124 additions & 3 deletions

File tree

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
516516
IList<IOpenApiSchema>? effectiveOneOf = OneOf;
517517
IList<IOpenApiSchema>? effectiveAnyOf = AnyOf;
518518
bool hasNullInComposition = false;
519+
bool hasOneOfNullAndSingleEnumWith3_0 = false;
519520
JsonSchemaType? inferredType = null;
520521

521522
if (version == OpenApiSpecVersion.OpenApi3_0)
@@ -526,6 +527,9 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
526527
(effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf);
527528
hasNullInComposition |= nullInAnyOf;
528529
inferredType = inferredAnyOf ?? inferredType;
530+
531+
hasOneOfNullAndSingleEnumWith3_0 = nullInOneOf && effectiveOneOf is { Count: 1 } &&
532+
effectiveOneOf[0].Enum is { Count: > 0 };
529533
}
530534

531535
// type
@@ -538,7 +542,27 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
538542
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback);
539543

540544
// oneOf
541-
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
545+
if (hasOneOfNullAndSingleEnumWith3_0)
546+
{
547+
writer.WriteRequiredCollection(OpenApiConstants.OneOf, effectiveOneOf!, (writer, element) =>
548+
{
549+
var clonedToMutateEnum = element.CreateShallowCopy();
550+
if (clonedToMutateEnum is OpenApiSchema { Enum: { } existingEnum } concreteCloned)
551+
{
552+
concreteCloned.Enum = [.. existingEnum, JsonNullSentinel.JsonNull];
553+
callback(writer, clonedToMutateEnum);
554+
}
555+
else
556+
{
557+
callback(writer, element);
558+
}
559+
});
560+
}
561+
else
562+
{
563+
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
564+
}
565+
542566

543567
// not
544568
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
@@ -1065,10 +1089,32 @@ private static (IList<IOpenApiSchema>? effective, JsonSchemaType? inferredType,
10651089

10661090
foreach (var schema in nonNullSchemas)
10671091
{
1068-
commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null;
1092+
if (schema.Type.HasValue)
1093+
{
1094+
commonType |= schema.Type.Value & ~JsonSchemaType.Null;
1095+
}
1096+
else if (schema.Enum is { Count: > 0 })
1097+
{
1098+
foreach (var enumValue in schema.Enum.Where(x => x is not null))
1099+
{
1100+
var currentType = enumValue.GetValueKind() switch
1101+
{
1102+
JsonValueKind.Array => JsonSchemaType.Array,
1103+
JsonValueKind.String => JsonSchemaType.String,
1104+
JsonValueKind.Number => JsonSchemaType.Number,
1105+
JsonValueKind.True or JsonValueKind.False => JsonSchemaType.Boolean,
1106+
JsonValueKind.Null => (JsonSchemaType)0,
1107+
_ => JsonSchemaType.Object,
1108+
};
1109+
1110+
commonType |= currentType;
1111+
}
1112+
1113+
commonType |= JsonSchemaType.String;
1114+
}
10691115
}
10701116

1071-
return (nonNullSchemas, commonType, true);
1117+
return (nonNullSchemas, commonType == 0 ? null : commonType, true);
10721118
}
10731119
else
10741120
{

test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
using System.Collections.Generic;
66
using System.Globalization;
77
using System.IO;
8+
using System.Text.Json;
89
using System.Text.Json.Nodes;
10+
using System.Text.Json.Schema;
11+
using System.Text.Json.Serialization;
912
using System.Threading.Tasks;
1013
using FluentAssertions;
1114
using VerifyXunit;
@@ -1847,6 +1850,78 @@ public void DeserializeContainsExtensionsInV3AssignsContainsProperties()
18471850
Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey(OpenApiConstants.MinContainsExtension));
18481851
}
18491852

1853+
[Fact]
1854+
public async Task SerializeNullableEnumWith3_0()
1855+
{
1856+
// https://spec.openapis.org/oas/v3.0.4.html#fixed-fields-20
1857+
// Documentation for nullable states:
1858+
// This keyword only takes effect if type is explicitly defined within the same Schema Object.
1859+
// So, we want to ensure that we emit the type property if we will be adding nullable property.
1860+
// In addition, we need to still keep 'null' in the enum array.
1861+
// Otherwise, validators will consider null as invalid even if nullable is set to true.
1862+
// It's unclear if it's an issue of the validators or not, but it's safer to do it that way.
1863+
var schema = CreateNullableEnumSchema();
1864+
var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
1865+
var expected = """
1866+
{
1867+
"type": "string",
1868+
"oneOf": [
1869+
{
1870+
"enum": [
1871+
"A",
1872+
"B",
1873+
null
1874+
]
1875+
}
1876+
],
1877+
"nullable": true
1878+
}
1879+
""";
1880+
1881+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
1882+
}
1883+
1884+
[Theory]
1885+
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
1886+
[InlineData(OpenApiSpecVersion.OpenApi3_2)]
1887+
public async Task SerializeNullableEnumWith3_1_And_Later(OpenApiSpecVersion version)
1888+
{
1889+
var schema = CreateNullableEnumSchema();
1890+
var result = await schema.SerializeAsJsonAsync(version);
1891+
var expected = """
1892+
{
1893+
"oneOf": [
1894+
{
1895+
"type": "null"
1896+
},
1897+
{
1898+
"enum": [
1899+
"A",
1900+
"B"
1901+
]
1902+
}
1903+
]
1904+
}
1905+
""";
1906+
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
1907+
}
1908+
1909+
private OpenApiSchema CreateNullableEnumSchema()
1910+
{
1911+
var schema = new OpenApiSchema();
1912+
schema.OneOf ??= [];
1913+
schema.OneOf.Add(new OpenApiSchema() { Type = JsonSchemaType.Null });
1914+
schema.OneOf.Add(new OpenApiSchema()
1915+
{
1916+
Enum = new List<JsonNode>
1917+
{
1918+
JsonValue.Create("A"),
1919+
JsonValue.Create("B")
1920+
}
1921+
});
1922+
return schema;
1923+
}
1924+
18501925
internal class SchemaVisitor : OpenApiVisitorBase
18511926
{
18521927
public List<string> Titles = new();

0 commit comments

Comments
 (0)