Skip to content
Open
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
86 changes: 74 additions & 12 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,23 +438,39 @@

// enum
var enumValue = Enum is not { Count: > 0 }
&& !string.IsNullOrEmpty(Const)
&& !string.IsNullOrEmpty(Const)
&& version < OpenApiSpecVersion.OpenApi3_1
? new List<JsonNode> { JsonValue.Create(Const)! }
: Enum;
writer.WriteOptionalCollection(OpenApiConstants.Enum, enumValue, (nodeWriter, s) => nodeWriter.WriteAny(s));

// Handle oneOf/anyOf with null type for v3.0 downcast
IList<IOpenApiSchema>? effectiveOneOf = OneOf;
IList<IOpenApiSchema>? effectiveAnyOf = AnyOf;
bool hasNullInComposition = false;
JsonSchemaType? inferredType = null;

if (version == OpenApiSpecVersion.OpenApi3_0)
{
(effectiveOneOf, var inferredOneOf, var nullInOneOf) = ProcessCompositionForNull(OneOf);
hasNullInComposition |= nullInOneOf;
inferredType = inferredOneOf ?? inferredType;
(effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf);
hasNullInComposition |= nullInAnyOf;
inferredType = inferredAnyOf ?? inferredType;
}

// type
SerializeTypeProperty(writer, version);
SerializeTypeProperty(writer, version, inferredType);

// allOf
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);

// anyOf
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback);
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback);

// oneOf
writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback);
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);

// not
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
Expand Down Expand Up @@ -493,7 +509,7 @@
// nullable
if (version == OpenApiSpecVersion.OpenApi3_0)
{
SerializeNullable(writer, version);
SerializeNullable(writer, version, hasNullInComposition);
}

// discriminator
Expand Down Expand Up @@ -766,14 +782,17 @@
writer.WriteEndObject();
}

private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version)
private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null)
{
if (Type is null)
// Use original type or inferred type when the explicit type is not set
var typeToUse = Type ?? inferredType;

if (typeToUse is null)
{
return;
}

var unifiedType = IsNullable ? Type.Value | JsonSchemaType.Null : Type.Value;
var unifiedType = IsNullable ? typeToUse.Value | JsonSchemaType.Null : typeToUse.Value;
var typeWithoutNull = unifiedType & ~JsonSchemaType.Null;

switch (version)
Expand Down Expand Up @@ -804,8 +823,8 @@
private static void WriteUnifiedSchemaType(JsonSchemaType type, IOpenApiWriter writer)
{
var array = (from JsonSchemaType flag in jsonSchemaTypeValues
where type.HasFlag(flag)
select flag.ToFirstIdentifier()).ToArray();
where type.HasFlag(flag)
select flag.ToFirstIdentifier()).ToArray();
if (array.Length > 1)
{
writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) =>
Expand All @@ -822,9 +841,9 @@
}
}

private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version, bool hasNullInComposition = false)
{
if (IsNullable)
if (IsNullable || hasNullInComposition)
{
switch (version)
{
Expand All @@ -838,6 +857,49 @@
}
}

/// <summary>
/// Processes a composition (oneOf or anyOf) for null types, filtering out null schemas and inferring common type.
/// </summary>
/// <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)
{
if (composition is null || !composition.Any(static s => s.Type is JsonSchemaType.Null))
{
// Nothing to patch
return (composition, null, false);
}

var nonNullSchemas = composition
.Where(static s => s.Type is null or not JsonSchemaType.Null)
.ToList();

if (nonNullSchemas.Count > 0)
{
JsonSchemaType commonType = 0;

foreach (var schema in nonNullSchemas)
{
commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null;
}

if (HasMultipleTypes(commonType) || commonType == 0)

Check warning

Code scanning / CodeQL

Constant condition Warning

Condition is always false because of
call to method HasMultipleTypes
.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... So HasMultipleTypes returns true for 0?? I don't think this is correct

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or at least, it would not match with the intent. If there are multiple or 0 we should not give a inferred type

{
return (nonNullSchemas, null, true);
}
else
{
// Single common type
return (nonNullSchemas, commonType, true);
}
Comment on lines +887 to +895

Check notice

Code scanning / CodeQL

Missed ternary opportunity Note

Both branches of this 'if' statement return - consider using '?' to express intent better.
}
else
{
return (null, null, true);
}
}

#if NET5_0_OR_GREATER
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues<JsonSchemaType>();
#else
Expand Down
Loading