Skip to content

Commit 165e0eb

Browse files
committed
Do not ignore multiple types when serializing to 3.0
1 parent 9fc177b commit 165e0eb

3 files changed

Lines changed: 572 additions & 41 deletions

File tree

src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,12 @@ internal static string ToFirstIdentifier(this JsonSchemaType schemaType)
6969
/// <returns></returns>
7070
internal static string ToSingleIdentifier(this JsonSchemaType schemaType)
7171
{
72-
return schemaType.ToIdentifiersInternal().Single();
72+
if (allSchemaTypes.TryGetValue(schemaType, out var schemaTypeString))
73+
{
74+
return schemaTypeString;
75+
}
76+
77+
throw new InvalidOperationException($"ToSingleIdentifier is called with unexpected value '{schemaType}'. Callers must ensure this is called with a valid single value JsonSchemaType.");
7378
}
7479

7580
/// <summary>

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace Microsoft.OpenApi
2121
/// </summary>
2222
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
2323
{
24-
private static readonly IEnumerable<JsonNode> s_singleNullElementList = [ JsonNullSentinel.JsonNull ];
24+
private static readonly IEnumerable<JsonNode> s_singleNullElementList = [JsonNullSentinel.JsonNull];
2525

2626
/// <inheritdoc />
2727
public string? Title { get; set; }
@@ -549,7 +549,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
549549
}
550550

551551
// type
552-
var serializedTypeProperty = TrySerializeTypeProperty(writer, version);
552+
var serializedTypeProperty = TrySerializeTypePropertyForVersion3AndLater(writer, version, callback);
553553

554554
// allOf
555555
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
@@ -681,7 +681,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action<IOpenApiWrit
681681
writer.WriteProperty(OpenApiConstants.Id, Id);
682682
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema?.ToString());
683683
writer.WriteProperty(OpenApiConstants.Comment, Comment);
684-
684+
685685
if (WasConstExplicitlySet)
686686
{
687687
writer.WriteRequiredProperty(OpenApiConstants.Const, Const);
@@ -692,7 +692,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action<IOpenApiWrit
692692
writer.WriteProperty(OpenApiConstants.Anchor, Anchor);
693693
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
694694
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);
695-
695+
696696
// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean.
697697
// Only emit when the type could include objects.
698698
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
@@ -833,7 +833,7 @@ private void SerializeAsV2(
833833
writer.WriteStartObject();
834834

835835
// type
836-
TrySerializeTypeProperty(writer, OpenApiSpecVersion.OpenApi2_0);
836+
TrySerializeTypePropertyForVersion2(writer);
837837

838838
// description
839839
writer.WriteProperty(OpenApiConstants.Description, Description);
@@ -915,7 +915,7 @@ private void SerializeAsV2(
915915
// oneOf (Not Supported in V2) - Write the first schema only as an allOf.
916916
writer.WriteOptionalCollection(OpenApiConstants.AllOf, OneOf?.Take(1), (w, s) => s.SerializeAsV2(w));
917917
}
918-
#pragma warning restore CS0618
918+
#pragma warning restore CS0618
919919
}
920920

921921
// properties
@@ -996,32 +996,103 @@ private void SerializeAsV2(
996996
writer.WriteEndObject();
997997
}
998998

999-
private bool TrySerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null)
999+
private bool TrySerializeTypePropertyForVersion2(IOpenApiWriter writer)
10001000
{
1001-
// Use original type or inferred type when the explicit type is not set
1002-
var typeToUse = Type ?? inferredType;
1001+
if (Type is not { } type)
1002+
{
1003+
return false;
1004+
}
1005+
1006+
var typeWithoutNull = type & ~JsonSchemaType.Null;
1007+
if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
1008+
{
1009+
writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
1010+
return true;
1011+
}
1012+
1013+
return false;
1014+
}
10031015

1004-
if (typeToUse is null)
1016+
/// <summary>
1017+
/// Tries to serialize the "type" property for OpenAPI v3 and later versions.
1018+
/// </summary>
1019+
/// <returns>
1020+
/// true if the Type was serializable using "type" property, and false if
1021+
/// it serialized using anyOf/oneOf or if it couldn't be serialized at all.
1022+
/// </returns>
1023+
private bool TrySerializeTypePropertyForVersion3AndLater(IOpenApiWriter writer, OpenApiSpecVersion version, Action<IOpenApiWriter, IOpenApiSerializable> callback)
1024+
{
1025+
if (Type is not { } type)
10051026
{
10061027
return false;
10071028
}
10081029

1009-
switch (version)
1030+
if (version == OpenApiSpecVersion.OpenApi3_0)
10101031
{
1011-
case OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0:
1012-
var typeWithoutNull = typeToUse.Value & ~JsonSchemaType.Null;
1013-
if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
1032+
if (type == JsonSchemaType.Null)
1033+
{
1034+
return false;
1035+
}
1036+
1037+
var typeWithoutNull = type & ~JsonSchemaType.Null;
1038+
var arrayWithoutNull = (from JsonSchemaType flag in jsonSchemaTypeValues
1039+
where typeWithoutNull.HasFlag(flag)
1040+
select flag).ToArray();
1041+
if (arrayWithoutNull.Length > 1)
1042+
{
1043+
// If the schema doesn't already have anyOf/oneOf, we can write multiple types as such.
1044+
var canWriteAsAnyOf = AnyOf is not { Count: > 0 };
1045+
var canWriteAsOneOf = OneOf is not { Count: > 0 };
1046+
if (canWriteAsAnyOf)
1047+
{
1048+
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, ConstructChildSchemasForTypes(arrayWithoutNull), callback);
1049+
return false;
1050+
}
1051+
else if (canWriteAsOneOf)
10141052
{
1015-
writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
1016-
return true;
1053+
writer.WriteOptionalCollection(OpenApiConstants.OneOf, ConstructChildSchemasForTypes(arrayWithoutNull), callback);
1054+
return false;
10171055
}
1018-
break;
1019-
default:
1020-
WriteUnifiedSchemaType(typeToUse.Value, writer);
1056+
}
1057+
else
1058+
{
1059+
writer.WriteProperty(OpenApiConstants.Type, arrayWithoutNull[0].ToSingleIdentifier());
10211060
return true;
1061+
}
1062+
}
1063+
else
1064+
{
1065+
var array = (from JsonSchemaType flag in jsonSchemaTypeValues
1066+
where type.HasFlag(flag)
1067+
select flag).ToArray();
1068+
1069+
if (array.Length > 1)
1070+
{
1071+
writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) => w.WriteValue(s.ToSingleIdentifier()));
1072+
}
1073+
else
1074+
{
1075+
writer.WriteProperty(OpenApiConstants.Type, array[0].ToSingleIdentifier());
1076+
}
1077+
1078+
return true;
10221079
}
10231080

10241081
return false;
1082+
1083+
static OpenApiSchema[] ConstructChildSchemasForTypes(JsonSchemaType[] types)
1084+
{
1085+
var schemas = new OpenApiSchema[types.Length];
1086+
for (int i = 0; i < types.Length; i++)
1087+
{
1088+
schemas[i] = new OpenApiSchema()
1089+
{
1090+
Type = types[i]
1091+
};
1092+
}
1093+
1094+
return schemas;
1095+
}
10251096
}
10261097

10271098
private static bool IsPowerOfTwo(int x)
@@ -1035,27 +1106,6 @@ private static bool HasMultipleTypes(JsonSchemaType schemaType)
10351106
return !IsPowerOfTwo(schemaTypeNumeric);
10361107
}
10371108

1038-
private static void WriteUnifiedSchemaType(JsonSchemaType type, IOpenApiWriter writer)
1039-
{
1040-
var array = (from JsonSchemaType flag in jsonSchemaTypeValues
1041-
where type.HasFlag(flag)
1042-
select flag.ToFirstIdentifier()).ToArray();
1043-
if (array.Length > 1)
1044-
{
1045-
writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) =>
1046-
{
1047-
if (!string.IsNullOrEmpty(s) && s is not null)
1048-
{
1049-
w.WriteValue(s);
1050-
}
1051-
});
1052-
}
1053-
else
1054-
{
1055-
writer.WriteProperty(OpenApiConstants.Type, array[0]);
1056-
}
1057-
}
1058-
10591109
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
10601110
{
10611111
if (HasNullType)

0 commit comments

Comments
 (0)