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 @@ -447,7 +447,7 @@
}
}

private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,

Check failure on line 450 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 30 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AZ8oZcBQJRrNf0DR9Dez&open=AZ8oZcBQJRrNf0DR9Dez&pullRequest=2927
Action<IOpenApiWriter, IOpenApiSerializable> callback)
{
writer.WriteStartObject();
Expand Down Expand Up @@ -516,6 +516,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 @@ -526,6 +527,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 };
}

// type
Expand All @@ -538,7 +542,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 @@ -1047,7 +1071,7 @@
/// <param name="composition">The list of schemas in the composition.</param>
/// <returns>A tuple with the effective list, inferred type, and whether null is present in composition.</returns>
private static (IList<IOpenApiSchema>? effective, JsonSchemaType? inferredType, bool hasNullInComposition)
ProcessCompositionForNull(IList<IOpenApiSchema>? composition)

Check failure on line 1074 in src/Microsoft.OpenApi/Models/OpenApiSchema.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AZ8oZcBQJRrNf0DR9De0&open=AZ8oZcBQJRrNf0DR9De0&pullRequest=2927
{
if (composition is null || !composition.Any(static s => s.Type is JsonSchemaType.Null))
{
Expand All @@ -1065,10 +1089,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;
}

commonType |= JsonSchemaType.String;
}
}

return (nonNullSchemas, commonType, true);
return (nonNullSchemas, commonType == 0 ? null : commonType, true);
}
else
{
Expand Down
74 changes: 74 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 @@ -1847,6 +1850,77 @@
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
]
}
],
"nullable": true
}
""";

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

[Theory]
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
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()

Check warning on line 1908 in test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'CreateNullableEnumSchema' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AZ8oZcCJJRrNf0DR9De1&open=AZ8oZcCJJRrNf0DR9De1&pullRequest=2927
{
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
Loading