Skip to content

Commit 782cf8d

Browse files
authored
Merge pull request #2645 from desjoerd/fix/support-v2/gh-2629
feat: Add `type: "null"` downcasting when in oneOf and anyOf for OpenAPI v3
1 parent 43ceb30 commit 782cf8d

File tree

2 files changed

+379
-12
lines changed

2 files changed

+379
-12
lines changed

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -438,23 +438,39 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
438438

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

447+
// Handle oneOf/anyOf with null type for v3.0 downcast
448+
IList<IOpenApiSchema>? effectiveOneOf = OneOf;
449+
IList<IOpenApiSchema>? effectiveAnyOf = AnyOf;
450+
bool hasNullInComposition = false;
451+
JsonSchemaType? inferredType = null;
452+
453+
if (version == OpenApiSpecVersion.OpenApi3_0)
454+
{
455+
(effectiveOneOf, var inferredOneOf, var nullInOneOf) = ProcessCompositionForNull(OneOf);
456+
hasNullInComposition |= nullInOneOf;
457+
inferredType = inferredOneOf ?? inferredType;
458+
(effectiveAnyOf, var inferredAnyOf, var nullInAnyOf) = ProcessCompositionForNull(AnyOf);
459+
hasNullInComposition |= nullInAnyOf;
460+
inferredType = inferredAnyOf ?? inferredType;
461+
}
462+
447463
// type
448-
SerializeTypeProperty(writer, version);
464+
SerializeTypeProperty(writer, version, inferredType);
449465

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

453469
// anyOf
454-
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, AnyOf, callback);
470+
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, effectiveAnyOf, callback);
455471

456472
// oneOf
457-
writer.WriteOptionalCollection(OpenApiConstants.OneOf, OneOf, callback);
473+
writer.WriteOptionalCollection(OpenApiConstants.OneOf, effectiveOneOf, callback);
458474

459475
// not
460476
writer.WriteOptionalObject(OpenApiConstants.Not, Not, callback);
@@ -493,7 +509,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
493509
// nullable
494510
if (version == OpenApiSpecVersion.OpenApi3_0)
495511
{
496-
SerializeNullable(writer, version);
512+
SerializeNullable(writer, version, hasNullInComposition);
497513
}
498514

499515
// discriminator
@@ -766,14 +782,17 @@ private void SerializeAsV2(
766782
writer.WriteEndObject();
767783
}
768784

769-
private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version)
785+
private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null)
770786
{
771-
if (Type is null)
787+
// Use original type or inferred type when the explicit type is not set
788+
var typeToUse = Type ?? inferredType;
789+
790+
if (typeToUse is null)
772791
{
773792
return;
774793
}
775794

776-
var unifiedType = IsNullable ? Type.Value | JsonSchemaType.Null : Type.Value;
795+
var unifiedType = IsNullable ? typeToUse.Value | JsonSchemaType.Null : typeToUse.Value;
777796
var typeWithoutNull = unifiedType & ~JsonSchemaType.Null;
778797

779798
switch (version)
@@ -804,8 +823,8 @@ private static bool HasMultipleTypes(JsonSchemaType schemaType)
804823
private static void WriteUnifiedSchemaType(JsonSchemaType type, IOpenApiWriter writer)
805824
{
806825
var array = (from JsonSchemaType flag in jsonSchemaTypeValues
807-
where type.HasFlag(flag)
808-
select flag.ToFirstIdentifier()).ToArray();
826+
where type.HasFlag(flag)
827+
select flag.ToFirstIdentifier()).ToArray();
809828
if (array.Length > 1)
810829
{
811830
writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) =>
@@ -822,9 +841,9 @@ where type.HasFlag(flag)
822841
}
823842
}
824843

825-
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
844+
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version, bool hasNullInComposition = false)
826845
{
827-
if (IsNullable)
846+
if (IsNullable || hasNullInComposition)
828847
{
829848
switch (version)
830849
{
@@ -838,6 +857,41 @@ private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version
838857
}
839858
}
840859

860+
/// <summary>
861+
/// Processes a composition (oneOf or anyOf) for null types, filtering out null schemas and inferring common type.
862+
/// </summary>
863+
/// <param name="composition">The list of schemas in the composition.</param>
864+
/// <returns>A tuple with the effective list, inferred type, and whether null is present in composition.</returns>
865+
private static (IList<IOpenApiSchema>? effective, JsonSchemaType? inferredType, bool hasNullInComposition)
866+
ProcessCompositionForNull(IList<IOpenApiSchema>? composition)
867+
{
868+
if (composition is null || !composition.Any(static s => s.Type is JsonSchemaType.Null))
869+
{
870+
// Nothing to patch
871+
return (composition, null, false);
872+
}
873+
874+
var nonNullSchemas = composition
875+
.Where(static s => s.Type is null or not JsonSchemaType.Null)
876+
.ToList();
877+
878+
if (nonNullSchemas.Count > 0)
879+
{
880+
JsonSchemaType commonType = 0;
881+
882+
foreach (var schema in nonNullSchemas)
883+
{
884+
commonType |= schema.Type.GetValueOrDefault() & ~JsonSchemaType.Null;
885+
}
886+
887+
return (nonNullSchemas, commonType, true);
888+
}
889+
else
890+
{
891+
return (null, null, true);
892+
}
893+
}
894+
841895
#if NET5_0_OR_GREATER
842896
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues<JsonSchemaType>();
843897
#else

0 commit comments

Comments
 (0)