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
7 changes: 6 additions & 1 deletion src/Microsoft.OpenApi/Extensions/OpenApiTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ internal static string ToFirstIdentifier(this JsonSchemaType schemaType)
/// <returns></returns>
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.");
}

/// <summary>
Expand Down
130 changes: 91 additions & 39 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Microsoft.OpenApi
/// </summary>
public class OpenApiSchema : IOpenApiExtensible, IOpenApiSchema, IOpenApiSchemaMissingProperties, IOpenApiSchemaWithUnevaluatedProperties, IMetadataContainer
{
private static readonly IEnumerable<JsonNode> s_singleNullElementList = [ JsonNullSentinel.JsonNull ];
private static readonly IEnumerable<JsonNode> s_singleNullElementList = [JsonNullSentinel.JsonNull];

/// <inheritdoc />
public string? Title { get; set; }
Expand Down Expand Up @@ -545,7 +545,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
}

// type
SerializeTypeProperty(writer, version);
SerializeTypePropertyForVersion3AndLater(writer, version, callback);

// allOf
writer.WriteOptionalCollection(OpenApiConstants.AllOf, AllOf, callback);
Expand Down Expand Up @@ -680,7 +680,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action<IOpenApiWrit
writer.WriteProperty(OpenApiConstants.Id, Id);
writer.WriteProperty(OpenApiConstants.DollarSchema, Schema?.ToString());
writer.WriteProperty(OpenApiConstants.Comment, Comment);

if (WasConstExplicitlySet)
{
writer.WriteRequiredProperty(OpenApiConstants.Const, Const);
Expand All @@ -691,7 +691,7 @@ internal void WriteJsonSchemaKeywords(IOpenApiWriter writer, Action<IOpenApiWrit
writer.WriteProperty(OpenApiConstants.Anchor, Anchor);
writer.WriteProperty(OpenApiConstants.DynamicRef, DynamicRef);
writer.WriteProperty(OpenApiConstants.DynamicAnchor, DynamicAnchor);

// UnevaluatedProperties: similar to AdditionalProperties, serialize as schema if present, else as boolean.
// Only emit when the type could include objects.
// Skip when type is explicitly set to a non-object type (array, string, number, integer, boolean, null).
Expand Down Expand Up @@ -833,7 +833,7 @@ private void SerializeAsV2(
writer.WriteStartObject();

// type
SerializeTypeProperty(writer, OpenApiSpecVersion.OpenApi2_0);
SerializeTypePropertyForVersion2(writer);

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

// properties
Expand Down Expand Up @@ -1001,31 +1001,104 @@ private void SerializeAsV2(
writer.WriteEndObject();
}

private void SerializeTypeProperty(IOpenApiWriter writer, OpenApiSpecVersion version)
private void SerializeTypePropertyForVersion2(IOpenApiWriter writer)
{
var typeToUse = Type;
if (Type is not { } type || type == JsonSchemaType.Null)
{
return;
}

var typeWithoutNull = type & ~JsonSchemaType.Null;
if (!HasMultipleTypes(typeWithoutNull))
{
writer.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
}
}

if (typeToUse is null)
/// <summary>
/// Serializes the "type" property for OpenAPI v3 and later versions,
/// falling back to anyOf/oneOf when multiple types cannot be expressed
/// using the "type" property alone (OpenAPI 3.0).
/// </summary>
private void SerializeTypePropertyForVersion3AndLater(IOpenApiWriter writer, OpenApiSpecVersion version, Action<IOpenApiWriter, IOpenApiSerializable> callback)
{
if (Type is not { } type)
{
return;
}

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;
}

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.WriteProperty(OpenApiConstants.Type, typeWithoutNull.ToFirstIdentifier());
writer.WriteOptionalCollection(OpenApiConstants.AnyOf, ConstructChildSchemasForTypes(arrayWithoutNull, hasNull), callback);
return;
}
break;
default:
WriteUnifiedSchemaType(typeToUse.Value, writer);
else if (canWriteAsOneOf)
{
writer.WriteOptionalCollection(OpenApiConstants.OneOf, ConstructChildSchemasForTypes(arrayWithoutNull, hasNull), callback);
return;
}
}
else if (arrayWithoutNull.Length == 1)
{
writer.WriteProperty(OpenApiConstants.Type, arrayWithoutNull[0].ToSingleIdentifier());
return;
}
}
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 if (array.Length == 1)
{
writer.WriteProperty(OpenApiConstants.Type, array[0].ToSingleIdentifier());
}
}

return;
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 JsonNode? GetCompatibilityExample()
Expand Down Expand Up @@ -1063,27 +1136,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)
Expand Down
106 changes: 106 additions & 0 deletions src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,113 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
schema.Type = JsonSchemaType.Null;
}

if (schema.Type is null)
{
if (schema.AnyOf is not null &&
schema.AnyOf.All(s => s is OpenApiSchema child && DoesSchemaRepresentSingleType(child)))
{
JsonSchemaType types = GetAllTypes(schema.AnyOf);
schema.AnyOf = null;
schema.Type = types;
}
else if (schema.OneOf is not null &&
schema.OneOf.All(s => s is OpenApiSchema child && DoesSchemaRepresentSingleType(child)))
{
JsonSchemaType types = GetAllTypes(schema.OneOf);
schema.OneOf = null;
schema.Type = types;
}
}

return schema;
}

private static JsonSchemaType GetAllTypes(IList<IOpenApiSchema> schemas)
{
JsonSchemaType types = 0;
foreach (var schema in schemas)
{
types |= schema.Type!.Value;
}

return types;
}

private static bool DoesSchemaRepresentSingleType(OpenApiSchema schema)
{
if (schema.Type is not (JsonSchemaType.Null or
JsonSchemaType.Boolean or
JsonSchemaType.Integer or
JsonSchemaType.Number or
JsonSchemaType.String or
JsonSchemaType.Object or
JsonSchemaType.Array))
{
return false;
}

// Folding anyOf/oneOf back into a single "type" is only safe when the child
// schema carries nothing but its type. Otherwise any additional keywords
// (format, bounds, enum, nested schemas, etc.) would be silently dropped.
// Metadata is intentionally ignored as it only holds internal bookkeeping.
return schema.Title is null &&
schema.Schema is null &&
schema.Id is null &&
schema.Comment is null &&
schema.Vocabulary is null &&
schema.DynamicRef is null &&
schema.DynamicAnchor is null &&
schema.Definitions is null &&
schema.Anchor is null &&
schema.Format is null &&
schema.Description is null &&
schema.Maximum is null &&
schema.Minimum is null &&
schema.ExclusiveMaximum is null &&
schema.ExclusiveMinimum is null &&
schema.MaxLength is null &&
schema.MinLength is null &&
schema.Pattern is null &&
schema.MultipleOf is null &&
schema.Default is null &&
!schema.ReadOnly &&
!schema.WriteOnly &&
schema.AllOf is null &&
schema.OneOf is null &&
schema.AnyOf is null &&
schema.Not is null &&
schema.Required is null &&
schema.Items is null &&
schema.MaxItems is null &&
schema.MinItems is null &&
schema.UniqueItems is null &&
schema.Contains is null &&
schema.MaxContains is null &&
schema.MinContains is null &&
schema.Properties is null &&
schema.PatternProperties is null &&
schema.MaxProperties is null &&
schema.MinProperties is null &&
schema.AdditionalPropertiesAllowed &&
schema.AdditionalProperties is null &&
schema.Discriminator is null &&
schema.Enum is null &&
schema.UnevaluatedProperties &&
schema.UnevaluatedPropertiesSchema is null &&
schema.ContentEncoding is null &&
schema.ContentMediaType is null &&
schema.ContentSchema is null &&
schema.PropertyNames is null &&
schema.DependentSchemas is null &&
schema.DependentRequired is null &&
schema.If is null &&
schema.Then is null &&
schema.Else is null &&
schema.ExternalDocs is null &&
!schema.Deprecated &&
schema.Xml is null &&
schema.Extensions is null &&
schema.UnrecognizedKeywords is null;
}
}
}
Loading