Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f4778e0

Browse filesBrowse 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
Copy full SHA for f4778e0

2 files changed

+124-3Lines changed: 124 additions & 3 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎src/Microsoft.OpenApi/Models/OpenApiSchema.cs‎

Copy file name to clipboardExpand all lines: src/Microsoft.OpenApi/Models/OpenApiSchema.cs
+49-3Lines 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
{
Collapse file

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

Copy file name to clipboardExpand all lines: test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
+75Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.