diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
index 8d557b4d1..1ad3a5b8f 100644
--- a/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
+++ b/src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
@@ -69,7 +69,12 @@ internal static string ToFirstIdentifier(this JsonSchemaType schemaType)
///
internal static string ToSingleIdentifier(this JsonSchemaType schemaType)
{
- return schemaType.ToIdentifiersInternal().Single();
+ if (allSchemaTypes.TryGetValue(schemaType, out var schemaTypeString))
+ {
+ return schemaTypeString;
+ }
+
+ throw new InvalidOperationException($"ToSingleIdentifier is called with unexpected value '{schemaType}'. Callers must ensure this is called with a valid single value JsonSchemaType.");
}
///
diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
index 2ae310294..5b80f8180 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs
@@ -21,7 +21,7 @@ namespace Microsoft.OpenApi
///
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
{
- private static readonly IEnumerable s_singleNullElementList = [ JsonNullSentinel.JsonNull ];
+ private static readonly IEnumerable s_singleNullElementList = [JsonNullSentinel.JsonNull];
///
public string? Title { get; set; }
@@ -549,7 +549,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
}
// type
- var serializedTypeProperty = TrySerializeTypeProperty(writer, version);
+ var serializedTypeProperty = TrySerializeTypePropertyForVersion3AndLater(writer, version, callback);
// allOf
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
@@ -681,7 +681,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action s.SerializeAsV2(w));
}
- #pragma warning restore CS0618
+#pragma warning restore CS0618
}
// properties
@@ -996,32 +996,113 @@ private void SerializeAsV2(
writer.WriteEndObject();
}
- private bool TrySerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version, JsonSchemaType? inferredType = null)
+ private bool TrySerializeTypePropertyForVersion2(IOpenApiWriter writer)
{
- // Use original type or inferred type when the explicit type is not set
- var typeToUse = Type ?? inferredType;
+ if (Type is not { } type)
+ {
+ return false;
+ }
- if (typeToUse is null)
+ var typeWithoutNull = type & ~JsonSchemaType.Null;
+ if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
+ {
+ writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Tries to serialize the "type" property for OpenAPI v3 and later versions.
+ ///
+ ///
+ /// true if the Type was serializable using "type" property, and false if
+ /// it serialized using anyOf/oneOf or if it couldn't be serialized at all.
+ ///
+ private bool TrySerializeTypePropertyForVersion3AndLater(IOpenApiWriter writer, OpenApiSpecVersion version, Action callback)
+ {
+ if (Type is not { } type)
{
return false;
}
- switch (version)
+ if (version == OpenApiSpecVersion.OpenApi3_0)
{
- case OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0:
- var typeWithoutNull = typeToUse.Value & ~JsonSchemaType.Null;
- if (typeWithoutNull != 0 && !HasMultipleTypes(typeWithoutNull))
+ if (type == JsonSchemaType.Null)
+ {
+ return false;
+ }
+
+ var typeWithoutNull = type & ~JsonSchemaType.Null;
+ var hasNull = typeWithoutNull != type;
+ var arrayWithoutNull = (from JsonSchemaType flag in jsonSchemaTypeValues
+ where typeWithoutNull.HasFlag(flag)
+ select flag).ToArray();
+
+ // - If we have more than one type (excluding null), we have to use anyOf/oneOf.
+ // - If we have exactly one type alone (without null), we emit the type property.
+ // - If we have exactly one non-null type and also we have the null type, we emit the type property and nullable: true (handled in SerializeNullable)
+ if (arrayWithoutNull.Length > 1)
+ {
+ // If the schema doesn't already have anyOf/oneOf, we can write multiple types as such.
+ var canWriteAsAnyOf = AnyOf is not { Count: > 0 };
+ var canWriteAsOneOf = OneOf is not { Count: > 0 };
+ if (canWriteAsAnyOf)
+ {
+ writer.WriteOptionalCollection(OpenApiConstants.AnyOf, ConstructChildSchemasForTypes(arrayWithoutNull, hasNull), callback);
+ return false;
+ }
+ else if (canWriteAsOneOf)
{
- writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
- return true;
+ writer.WriteOptionalCollection(OpenApiConstants.OneOf, ConstructChildSchemasForTypes(arrayWithoutNull, hasNull), callback);
+ return false;
}
- break;
- default:
- WriteUnifiedSchemaType(typeToUse.Value, writer);
+ }
+ else
+ {
+ writer.WriteProperty(OpenApiConstants.Type, arrayWithoutNull[0].ToSingleIdentifier());
return true;
+ }
+ }
+ else
+ {
+ var array = (from JsonSchemaType flag in jsonSchemaTypeValues
+ where type.HasFlag(flag)
+ select flag).ToArray();
+
+ if (array.Length > 1)
+ {
+ writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) => w.WriteValue(s.ToSingleIdentifier()));
+ }
+ else
+ {
+ writer.WriteProperty(OpenApiConstants.Type, array[0].ToSingleIdentifier());
+ }
+
+ return true;
}
return false;
+
+ static OpenApiSchema[] ConstructChildSchemasForTypes(JsonSchemaType[] types, bool hasNull)
+ {
+ var schemas = new OpenApiSchema[types.Length + (hasNull ? 1 : 0)];
+ for (int i = 0; i < types.Length; i++)
+ {
+ schemas[i] = new OpenApiSchema()
+ {
+ Type = types[i]
+ };
+ }
+
+ if (hasNull)
+ {
+ schemas[schemas.Length - 1] = new OpenApiSchema() { Type = JsonSchemaType.Null };
+ }
+
+ return schemas;
+ }
}
private static bool IsPowerOfTwo(int x)
@@ -1035,27 +1116,6 @@ private static bool HasMultipleTypes(JsonSchemaType schemaType)
return !IsPowerOfTwo(schemaTypeNumeric);
}
- private static void WriteUnifiedSchemaType(JsonSchemaType type, IOpenApiWriter writer)
- {
- var array = (from JsonSchemaType flag in jsonSchemaTypeValues
- where type.HasFlag(flag)
- select flag.ToFirstIdentifier()).ToArray();
- if (array.Length > 1)
- {
- writer.WriteOptionalCollection(OpenApiConstants.Type, array, (w, s) =>
- {
- if (!string.IsNullOrEmpty(s) && s is not null)
- {
- w.WriteValue(s);
- }
- });
- }
- else
- {
- writer.WriteProperty(OpenApiConstants.Type, array[0]);
- }
- }
-
private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version)
{
if (HasNullType)
diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
index b4b188617..f2c669f7c 100644
--- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
+++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs
@@ -1968,6 +1968,497 @@ public async Task SerializeNullableTypeWith3_1_And_Later(OpenApiSpecVersion vers
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
}
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_NotIncludingNull_NotIncludingOneOfOrAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer,
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_NotIncludingNull_IncludingOneOfButNotAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer,
+ OneOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "oneOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_NotIncludingNull_IncludingAnyOfButNotOneOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer,
+ AnyOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "oneOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "anyOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_NotIncludingNull_IncludingBothOneOfAndAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer,
+ AnyOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ OneOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "anyOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ],
+ "oneOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndSingleType_NotIncludingOneOfOrAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Null,
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "type": "string",
+ "nullable": true
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndSingleType_IncludingOneOfButNotAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Null,
+ OneOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "type": "string",
+ "oneOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ],
+ "nullable": true
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndSingleType_IncludingAnyOfButNotOneOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Null,
+ AnyOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "type": "string",
+ "anyOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ],
+ "nullable": true
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndSingleType_IncludingBothOneOfAndAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Null,
+ AnyOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ OneOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "type": "string",
+ "anyOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ],
+ "oneOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ],
+ "nullable": true
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndTwoTypes_NotIncludingOneOfOrAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ null
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndTwoTypes_IncludingOneOfButNotAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,
+ OneOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ null
+ ]
+ }
+ ],
+ "oneOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndTwoTypes_IncludingAnyOfButNotOneOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,
+ AnyOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "oneOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "enum": [
+ null
+ ]
+ }
+ ],
+ "anyOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
+ [Fact]
+ public async Task SerializeMultipleTypesWith3_0_IncludingNullAndTwoTypes_IncludingBothOneOfAndAnyOf()
+ {
+ var schema = new OpenApiSchema()
+ {
+ Type = JsonSchemaType.String | JsonSchemaType.Integer | JsonSchemaType.Null,
+ AnyOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ OneOf =
+ [
+ new OpenApiSchema()
+ {
+ Enum = new List
+ {
+ JsonValue.Create("A"),
+ JsonValue.Create("B")
+ }
+ },
+ ],
+ };
+ var result = await schema.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_0);
+ var expected = """
+ {
+ "anyOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ],
+ "oneOf": [
+ {
+ "enum": [
+ "A",
+ "B"
+ ]
+ }
+ ]
+ }
+ """;
+
+ Assert.True(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result)));
+ }
+
private OpenApiSchema CreateNullableEnumSchema()
{
var schema = new OpenApiSchema();