Skip to content

Spruce up OpenApiSchema generation #41468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 10, 2022
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
19 changes: 5 additions & 14 deletions src/OpenApi/src/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM
{
responseContent[contentType] = new OpenApiMediaType
{
Schema = new OpenApiSchema { Type = SchemaGenerator.GetOpenApiSchemaType(type) }
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(type)
};
}

Expand Down Expand Up @@ -271,10 +271,7 @@ private static void GenerateDefaultResponses(Dictionary<int, (Type?, MediaTypeCo
{
requestBodyContent[contentType] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = SchemaGenerator.GetOpenApiSchemaType(acceptsMetadata.RequestType ?? requestBodyParameter?.ParameterType)
}
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(acceptsMetadata.RequestType ?? requestBodyParameter?.ParameterType)
};
}
isRequired = !acceptsMetadata.IsOptional;
Expand All @@ -299,20 +296,14 @@ private static void GenerateDefaultResponses(Dictionary<int, (Type?, MediaTypeCo
{
requestBodyContent["multipart/form-data"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = SchemaGenerator.GetOpenApiSchemaType(requestBodyParameter.ParameterType)
}
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(requestBodyParameter.ParameterType)
};
}
else
{
requestBodyContent["application/json"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = SchemaGenerator.GetOpenApiSchemaType(requestBodyParameter.ParameterType)
}
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(requestBodyParameter.ParameterType)
};
}
}
Expand Down Expand Up @@ -380,7 +371,7 @@ private List<OpenApiParameter> GetOpenApiParameters(MethodInfo methodInfo, Endpo
Name = parameter.Name,
In = parameterLocation,
Content = GetOpenApiParameterContent(metadata),
Schema = new OpenApiSchema { Type = SchemaGenerator.GetOpenApiSchemaType(parameter.ParameterType) },
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(parameter.ParameterType),
Required = !isOptional

};
Expand Down
77 changes: 77 additions & 0 deletions src/OpenApi/src/OpenApiSchemaGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using Microsoft.AspNetCore.Http;
using Microsoft.OpenApi.Models;

namespace Microsoft.AspNetCore.OpenApi;

internal static class OpenApiSchemaGenerator
{
private static readonly Dictionary<Type, (string, string?)> simpleTypesAndFormats =
new()
{
[typeof(bool)] = ("boolean", null),
[typeof(byte)] = ("string", "byte"),
[typeof(int)] = ("integer", "int32"),
[typeof(uint)] = ("integer", "int32"),
[typeof(ushort)] = ("integer", "int32"),
[typeof(long)] = ("integer", "int64"),
[typeof(ulong)] = ("integer", "int64"),
[typeof(float)] = ("number", "float"),
[typeof(double)] = ("number", "double"),
[typeof(decimal)] = ("number", "double"),
[typeof(DateTime)] = ("string", "date-time"),
[typeof(DateTimeOffset)] = ("string", "date-time"),
[typeof(TimeSpan)] = ("string", "date-span"),
[typeof(Guid)] = ("string", "uuid"),
[typeof(char)] = ("string", null),
[typeof(Uri)] = ("string", "uri"),
[typeof(string)] = ("string", null),
[typeof(object)] = ("object", null)
};

internal static OpenApiSchema GetOpenApiSchema(Type? type)
{
if (type is null)
{
return new OpenApiSchema();
}

var (openApiType, openApiFormat) = GetTypeAndFormatProperties(type);
return new OpenApiSchema
{
Type = openApiType,
Format = openApiFormat,
Nullable = Nullable.GetUnderlyingType(type) != null,
};
}

private static (string, string?) GetTypeAndFormatProperties(Type type)
{
type = Nullable.GetUnderlyingType(type) ?? type;

if (simpleTypesAndFormats.TryGetValue(type, out var typeAndFormat))
{
return typeAndFormat;
}

if (type == typeof(IFormFileCollection) || type == typeof(IFormFile))
{
return ("object", null);
}

if (typeof(IDictionary).IsAssignableFrom(type))
{
return ("object", null);
}

if (type != typeof(string) && (type.IsArray || typeof(IEnumerable).IsAssignableFrom(type)))
{
return ("array", null);
}

return ("object", null);
}
}
44 changes: 0 additions & 44 deletions src/OpenApi/src/SchemaGenerator.cs

This file was deleted.

27 changes: 14 additions & 13 deletions src/OpenApi/test/OpenApiGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public void AddsMultipleResponseFormatsFromMetadataWithPoco()
var content = Assert.Single(createdResponseType.Content);

Assert.NotNull(createdResponseType);
Assert.Equal("object", content.Value.Schema.Type);
Assert.Equal("string", content.Value.Schema.Type);
Assert.Equal("application/json", createdResponseType.Content.Keys.First());

var badRequestResponseType = responses["400"];
Expand Down Expand Up @@ -209,7 +209,7 @@ public void AddsFromRouteParameterAsPath()
static void AssertPathParameter(OpenApiOperation operation)
{
var param = Assert.Single(operation.Parameters);
Assert.Equal("number", param.Schema.Type);
Assert.Equal("integer", param.Schema.Type);
Assert.Equal(ParameterLocation.Path, param.In);
}

Expand All @@ -235,7 +235,7 @@ public void AddsFromRouteParameterAsPathWithNullablePrimitiveType()
static void AssertPathParameter(OpenApiOperation operation)
{
var param = Assert.Single(operation.Parameters);
Assert.Equal("number", param.Schema.Type);
Assert.Equal("integer", param.Schema.Type);
Assert.Equal(ParameterLocation.Path, param.In);
}

Expand Down Expand Up @@ -265,12 +265,12 @@ static void AssertQueryParameter(OpenApiOperation operation, string type)
Assert.Equal(ParameterLocation.Query, param.In);
}

AssertQueryParameter(GetOpenApiOperation((int foo) => { }, "/"), "number");
AssertQueryParameter(GetOpenApiOperation(([FromQuery] int foo) => { }), "number");
AssertQueryParameter(GetOpenApiOperation((int foo) => { }, "/"), "integer");
AssertQueryParameter(GetOpenApiOperation(([FromQuery] int foo) => { }), "integer");
AssertQueryParameter(GetOpenApiOperation(([FromQuery] TryParseStringRecordStruct foo) => { }), "object");
AssertQueryParameter(GetOpenApiOperation((int[] foo) => { }, "/"), "array");
AssertQueryParameter(GetOpenApiOperation((string[] foo) => { }, "/"), "array");
AssertQueryParameter(GetOpenApiOperation((StringValues foo) => { }, "/"), "object");
AssertQueryParameter(GetOpenApiOperation((StringValues foo) => { }, "/"), "array");
AssertQueryParameter(GetOpenApiOperation((TryParseStringRecordStruct[] foo) => { }, "/"), "array");
}

Expand All @@ -297,7 +297,7 @@ public void AddsFromHeaderParameterAsHeader()
var operation = GetOpenApiOperation(([FromHeader] int foo) => { });
var param = Assert.Single(operation.Parameters);

Assert.Equal("number", param.Schema.Type);
Assert.Equal("integer", param.Schema.Type);
Assert.Equal(ParameterLocation.Header, param.In);
}

Expand Down Expand Up @@ -325,7 +325,7 @@ static void AssertBodyParameter(OpenApiOperation operation, string expectedName,
}

AssertBodyParameter(GetOpenApiOperation((InferredJsonClass foo) => { }), "foo", "object");
AssertBodyParameter(GetOpenApiOperation(([FromBody] int bar) => { }), "bar", "number");
AssertBodyParameter(GetOpenApiOperation(([FromBody] int bar) => { }), "bar", "integer");
}

#nullable enable
Expand All @@ -338,13 +338,13 @@ public void AddsMultipleParameters()

var fooParam = operation.Parameters[0];
Assert.Equal("foo", fooParam.Name);
Assert.Equal("number", fooParam.Schema.Type);
Assert.Equal("integer", fooParam.Schema.Type);
Assert.Equal(ParameterLocation.Path, fooParam.In);
Assert.True(fooParam.Required);

var barParam = operation.Parameters[1];
Assert.Equal("bar", barParam.Name);
Assert.Equal("number", barParam.Schema.Type);
Assert.Equal("integer", barParam.Schema.Type);
Assert.Equal(ParameterLocation.Query, barParam.In);
Assert.True(barParam.Required);

Expand All @@ -363,13 +363,14 @@ public void TestParameterIsRequired()

var fooParam = operation.Parameters[0];
Assert.Equal("foo", fooParam.Name);
Assert.Equal("number", fooParam.Schema.Type);
Assert.Equal("integer", fooParam.Schema.Type);
Assert.Equal(ParameterLocation.Path, fooParam.In);
Assert.True(fooParam.Required);

var barParam = operation.Parameters[1];
Assert.Equal("bar", barParam.Name);
Assert.Equal("number", barParam.Schema.Type);
Assert.Equal("integer", barParam.Schema.Type);
Assert.True(barParam.Schema.Nullable);
Assert.Equal(ParameterLocation.Query, barParam.In);
Assert.False(barParam.Required);
}
Expand All @@ -388,7 +389,7 @@ public void TestParameterIsRequiredForObliviousNullabilityContext()
Assert.False(fooParam.Required);

var barParam = operation.Parameters[1];
Assert.Equal("number", barParam.Schema.Type);
Assert.Equal("integer", barParam.Schema.Type);
Assert.Equal(ParameterLocation.Query, barParam.In);
Assert.True(barParam.Required);
}
Expand Down
111 changes: 111 additions & 0 deletions src/OpenApi/test/OpenApiSchemaGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.OpenApi;

namespace Microsoft.AspNetCore.OpenApi.Tests;

public class OpenApiSchemaGeneratorTests
{
[Theory]
[InlineData(typeof(Dictionary<string, string>))]
[InlineData(typeof(Todo))]
public void CanGenerateCorrectSchemaForDictionaryTypes(Type type)
{
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(type);
Assert.NotNull(schema);
Assert.Equal("object", schema.Type);
}

[Theory]
[InlineData(typeof(IList<string>))]
[InlineData(typeof(Products))]
public void CanGenerateSchemaForListTypes(Type type)
{
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(type);
Assert.NotNull(schema);
Assert.Equal("array", schema.Type);
}

[Theory]
[InlineData(typeof(DateTime))]
[InlineData(typeof(DateTimeOffset))]
public void CanGenerateSchemaForDateTimeTypes(Type type)
{
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(type);
Assert.NotNull(schema);
Assert.Equal("string", schema.Type);
Assert.Equal("date-time", schema.Format);
}

[Fact]
public void CanGenerateSchemaForDateSpanTypes()
{
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(typeof(TimeSpan));
Assert.NotNull(schema);
Assert.Equal("string", schema.Type);
Assert.Equal("date-span", schema.Format);
}

class Todo : Dictionary<string, object> { }
class Products : IList<int>
{
public int this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

public int Count => throw new NotImplementedException();

public bool IsReadOnly => throw new NotImplementedException();

public void Add(int item)
{
throw new NotImplementedException();
}

public void Clear()
{
throw new NotImplementedException();
}

public bool Contains(int item)
{
throw new NotImplementedException();
}

public void CopyTo(int[] array, int arrayIndex)
{
throw new NotImplementedException();
}

public IEnumerator<int> GetEnumerator()
{
throw new NotImplementedException();
}

public int IndexOf(int item)
{
throw new NotImplementedException();
}

public void Insert(int index, int item)
{
throw new NotImplementedException();
}

public bool Remove(int item)
{
throw new NotImplementedException();
}

public void RemoveAt(int index)
{
throw new NotImplementedException();
}

IEnumerator IEnumerable.GetEnumerator()
{
throw new NotImplementedException();
}
}
}