Skip to content

Commit 21bcff8

Browse files
committed
Fix handling of nullable enums for 3.0
1 parent 3764142 commit 21bcff8

2 files changed

Lines changed: 106 additions & 3 deletions

File tree

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 29 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,22 @@ 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+
effectiveOneOf![0] is OpenApiSchema { Enum.Count: > 0 } singleEffectiveOneOf)
552+
{
553+
var tempCloned = (OpenApiSchema)singleEffectiveOneOf.MemberwiseClone();
554+
writer.WriteRequiredCollection(OpenApiConstants.OneOf, effectiveOneOf, (writer, element) =>
555+
{
556+
var clonedToMutateEnum = (OpenApiSchema)((OpenApiSchema)element).MemberwiseClone();
557+
clonedToMutateEnum.Enum = [.. clonedToMutateEnum.Enum!, null!];
558+
callback(writer, clonedToMutateEnum);
559+
});
560+
}
561+
else
562+
{
563+
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
564+
}
565+
547566

548567
// not
549568
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
@@ -1070,10 +1089,17 @@ private static (IList<IOpenApiSchema>? effective, JsonSchemaType? inferredType,
10701089

10711090
foreach (var schema in nonNullSchemas)
10721091
{
1073-
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+
commonType |= JsonSchemaType.String;
1099+
}
10741100
}
10751101

1076-
return (nonNullSchemas, commonType, true);
1102+
return (nonNullSchemas, commonType == 0 ? null : commonType, true);
10771103
}
10781104
else
10791105
{

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

Lines changed: 77 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,80 @@ 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+
Assert.Equal("""
1872+
{
1873+
"type": "string",
1874+
"oneOf": [
1875+
{
1876+
"enum": [
1877+
"A",
1878+
"B",
1879+
null
1880+
]
1881+
}
1882+
],
1883+
"nullable": true
1884+
}
1885+
""".ReplaceLineEndings(), result.ReplaceLineEndings());
1886+
}
1887+
1888+
[Theory]
1889+
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
1890+
[InlineData(OpenApiSpecVersion.OpenApi3_2)]
1891+
public async Task SerializeNullableEnumWith3_1_And_Later(OpenApiSpecVersion version)
1892+
{
1893+
var schema = CreateNullableEnumSchema();
1894+
var result = await schema.SerializeAsJsonAsync(version);
1895+
Assert.Equal("""
1896+
{
1897+
"oneOf": [
1898+
{
1899+
"type": "null"
1900+
},
1901+
{
1902+
"enum": [
1903+
"A",
1904+
"B"
1905+
]
1906+
}
1907+
]
1908+
}
1909+
""".ReplaceLineEndings(), result.ReplaceLineEndings());
1910+
}
1911+
1912+
private OpenApiSchema CreateNullableEnumSchema()
1913+
{
1914+
var schema = new OpenApiSchema();
1915+
schema.OneOf ??= [];
1916+
schema.OneOf.Add(new OpenApiSchema() { Type = JsonSchemaType.Null });
1917+
schema.OneOf.Add(new OpenApiSchema()
1918+
{
1919+
Enum = new List<JsonNode>
1920+
{
1921+
JsonValue.Create("A"),
1922+
JsonValue.Create("B")
1923+
}
1924+
});
1925+
return schema;
1926+
}
1927+
1928+
private enum MyEnum
1929+
{
1930+
A, B
1931+
}
1932+
18561933
internal class SchemaVisitor : OpenApiVisitorBase
18571934
{
18581935
public List<string> Titles = new();

0 commit comments

Comments
 (0)