Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@
IList<IOpenApiSchema>? effectiveOneOf = OneOf;
IList<IOpenApiSchema>? effectiveAnyOf = AnyOf;
bool hasNullInComposition = false;
bool hasOneOfNullAndSingleEnumWith3_0 = false;
JsonSchemaType? inferredType = null;

if (version == OpenApiSpecVersion.OpenApi3_0)
Expand All @@ -531,6 +532,9 @@
(effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf);
hasNullInComposition |= nullInAnyOf;
inferredType = inferredAnyOf ?? inferredType;

hasOneOfNullAndSingleEnumWith3_0 = nullInOneOf && effectiveOneOf is { Count: 1 } &&
effectiveOneOf[0].Enum is { Count: > 0 };
Comment thread
baywet marked this conversation as resolved.
}

// type
Expand All @@ -543,7 +547,27 @@
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback);

// oneOf
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
if (hasOneOfNullAndSingleEnumWith3_0)
{
writer.WriteRequiredCollection(OpenApiConstants.OneOf, effectiveOneOf!, (writer, element) =>
{
var clonedToMutateEnum = element.CreateShallowCopy();
if (clonedToMutateEnum is OpenApiSchema { Enum: { } existingEnum } concreteCloned)
{
concreteCloned.Enum = [.. existingEnum, JsonNullSentinel.JsonNull];
callback(writer, clonedToMutateEnum);
}
else
{
callback(writer, element);
}
});
}
else
{
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
}


// not
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
Expand Down Expand Up @@ -1070,10 +1094,32 @@

foreach (var schema in nonNullSchemas)
{
commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null;
if (schema.Type.HasValue)
{
commonType |= schema.Type.Value & ~JsonSchemaType.Null;
}
else if (schema.Enum is { Count: > 0 })
{
foreach (var enumValue in schema.Enum.Where(x => x is not null))
{
var currentType = enumValue.GetValueKind() switch
{
JsonValueKind.Array => JsonSchemaType.Array,
JsonValueKind.String => JsonSchemaType.String,
JsonValueKind.Number => JsonSchemaType.Number,
JsonValueKind.True or JsonValueKind.False => JsonSchemaType.Boolean,
JsonValueKind.Null => (JsonSchemaType)0,
_ => JsonSchemaType.Object,
};

commonType |= currentType;
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Select Note

This foreach loop immediately
maps its iteration variable to another variable
- consider mapping the sequence explicitly using '.Select(...)'.
Comment thread
baywet marked this conversation as resolved.
Dismissed

commonType |= JsonSchemaType.String;
}
Comment thread
Youssef1313 marked this conversation as resolved.
}

return (nonNullSchemas, commonType, true);
return (nonNullSchemas, commonType == 0 ? null : commonType, true);
}
else
{
Expand Down
75 changes: 75 additions & 0 deletions test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using FluentAssertions;
using VerifyXunit;
Expand Down Expand Up @@ -1853,6 +1856,78 @@ public void DeserializeContainsExtensionsInV3AssignsContainsProperties()
Assert.True(schema.Extensions is null || !schema.Extensions.ContainsKey(OpenApiConstants.MinContainsExtension));
}

[Fact]
public async Task SerializeNullableEnumWith3_0()
{
// https://spec.openapis.org/oas/v3.0.4.html#fixed-fields-20
// Documentation for nullable states:
// This keyword only takes effect if type is explicitly defined within the same Schema Object.
// So, we want to ensure that we emit the type property if we will be adding nullable property.
// In addition, we need to still keep 'null' in the enum array.
// Otherwise, validators will consider null as invalid even if nullable is set to true.
// It's unclear if it's an issue of the validators or not, but it's safer to do it that way.
var schema = CreateNullableEnumSchema();
var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
var expected = """
{
"type": "string",
"oneOf": [
{
"enum": [
"A",
"B",
null
]
Comment thread
baywet marked this conversation as resolved.
}
],
"nullable": true
}
""";

Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
}

[Theory]
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
[InlineData(OpenApiSpecVersion.OpenApi3_2)]
public async Task SerializeNullableEnumWith3_1_And_Later(OpenApiSpecVersion version)
{
var schema = CreateNullableEnumSchema();
var result = await schema.SerializeAsJsonAsync(version);
var expected = """
{
"oneOf": [
{
"type": "null"
},
{
"enum": [
"A",
"B"
]
}
]
}
""";
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
}

private OpenApiSchema CreateNullableEnumSchema()
{
var schema = new OpenApiSchema();
schema.OneOf ??= [];
schema.OneOf.Add(new OpenApiSchema() { Type = JsonSchemaType.Null });
schema.OneOf.Add(new OpenApiSchema()
{
Enum = new List<JsonNode>
{
JsonValue.Create("A"),
JsonValue.Create("B")
}
});
return schema;
}

internal class SchemaVisitor : OpenApiVisitorBase
{
public List<string> Titles = new();
Expand Down