Skip to content

Commit beb68f5

Browse files
authored
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 ce09f41 commit beb68f5

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
@@ -521,6 +521,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
521521
IList<IOpenApiSchema>? effectiveOneOf = OneOf;
522522
IList<IOpenApiSchema>? effectiveAnyOf = AnyOf;
523523
bool hasNullInComposition = false;
524+
bool hasOneOfNullAndSingleEnumWith3_0 = false;
524525
JsonSchemaType? inferredType = null;
525526

526527
if (version == OpenApiSpecVersion.OpenApi3_0)
@@ -531,6 +532,9 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
531532
(effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf);
532533
hasNullInComposition |= nullInAnyOf;
533534
inferredType = inferredAnyOf ?? inferredType;
535+
536+
hasOneOfNullAndSingleEnumWith3_0 = nullInOneOf && effectiveOneOf is { Count: 1 } &&
537+
effectiveOneOf[0].Enum is { Count: > 0 };
534538
}
535539

536540
// type
@@ -543,7 +547,27 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
543547
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback);
544548

545549
// oneOf
546-
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
550+
if (hasOneOfNullAndSingleEnumWith3_0)
551+
{
552+
writer.WriteRequiredCollection(OpenApiConstants.OneOf, effectiveOneOf!, (writer, element) =>
553+
{
554+
var clonedToMutateEnum = element.CreateShallowCopy();
555+
if (clonedToMutateEnum is OpenApiSchema { Enum: { } existingEnum } concreteCloned)
556+
{
557+
concreteCloned.Enum = [.. existingEnum, JsonNullSentinel.JsonNull];
558+
callback(writer, clonedToMutateEnum);
559+
}
560+
else
561+
{
562+
callback(writer, element);
563+
}
564+
});
565+
}
566+
else
567+
{
568+
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
569+
}
570+
547571

548572
// not
549573
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
@@ -1070,10 +1094,32 @@ private static (IList<IOpenApiSchema>? effective, JsonSchemaType? inferredType,
10701094

10711095
foreach (var schema in nonNullSchemas)
10721096
{
1073-
commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null;
1097+
if (schema.Type.HasValue)
1098+
{
1099+
commonType |= schema.Type.Value & ~JsonSchemaType.Null;
1100+
}
1101+
else if (schema.Enum is { Count: > 0 })
1102+
{
1103+
foreach (var enumValue in schema.Enum.Where(x => x is not null))
1104+
{
1105+
var currentType = enumValue.GetValueKind() switch
1106+
{
1107+
JsonValueKind.Array => JsonSchemaType.Array,
1108+
JsonValueKind.String => JsonSchemaType.String,
1109+
JsonValueKind.Number => JsonSchemaType.Number,
1110+
JsonValueKind.True or JsonValueKind.False => JsonSchemaType.Boolean,
1111+
JsonValueKind.Null => (JsonSchemaType)0,
1112+
_ => JsonSchemaType.Object,
1113+
};
1114+
1115+
commonType |= currentType;
1116+
}
1117+
1118+
commonType |= JsonSchemaType.String;
1119+
}
10741120
}
10751121

1076-
return (nonNullSchemas, commonType, true);
1122+
return (nonNullSchemas, commonType == 0 ? null : commonType, true);
10771123
}
10781124
else
10791125
{

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;
@@ -1853,6 +1856,78 @@ public void DeserializeContainsExtensionsInV3AssignsContainsProperties()
18531856
Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey(OpenApiConstants.MinContainsExtension));
18541857
}
18551858

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

0 commit comments

Comments
 (0)