Skip to content

Commit 52c11c3

Browse files
authored
Spruce up OpenApiSchema generation (#41468)
* Spruce up OpenApiSchema generation * Support TimeSpans, dictionaries, and forms * Update tests
1 parent d1dff43 commit 52c11c3

5 files changed

+207
-71
lines changed

src/OpenApi/src/OpenApiGenerator.cs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ private static OpenApiResponses GetOpenApiResponses(MethodInfo method, EndpointM
200200
{
201201
responseContent[contentType] = new OpenApiMediaType
202202
{
203-
Schema = new OpenApiSchema { Type = SchemaGenerator.GetOpenApiSchemaType(type) }
203+
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(type)
204204
};
205205
}
206206

@@ -271,10 +271,7 @@ private static void GenerateDefaultResponses(Dictionary<int, (Type?, MediaTypeCo
271271
{
272272
requestBodyContent[contentType] = new OpenApiMediaType
273273
{
274-
Schema = new OpenApiSchema
275-
{
276-
Type = SchemaGenerator.GetOpenApiSchemaType(acceptsMetadata.RequestType ?? requestBodyParameter?.ParameterType)
277-
}
274+
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(acceptsMetadata.RequestType ?? requestBodyParameter?.ParameterType)
278275
};
279276
}
280277
isRequired = !acceptsMetadata.IsOptional;
@@ -299,20 +296,14 @@ private static void GenerateDefaultResponses(Dictionary<int, (Type?, MediaTypeCo
299296
{
300297
requestBodyContent["multipart/form-data"] = new OpenApiMediaType
301298
{
302-
Schema = new OpenApiSchema
303-
{
304-
Type = SchemaGenerator.GetOpenApiSchemaType(requestBodyParameter.ParameterType)
305-
}
299+
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(requestBodyParameter.ParameterType)
306300
};
307301
}
308302
else
309303
{
310304
requestBodyContent["application/json"] = new OpenApiMediaType
311305
{
312-
Schema = new OpenApiSchema
313-
{
314-
Type = SchemaGenerator.GetOpenApiSchemaType(requestBodyParameter.ParameterType)
315-
}
306+
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(requestBodyParameter.ParameterType)
316307
};
317308
}
318309
}
@@ -380,7 +371,7 @@ private List<OpenApiParameter> GetOpenApiParameters(MethodInfo methodInfo, Endpo
380371
Name = parameter.Name,
381372
In = parameterLocation,
382373
Content = GetOpenApiParameterContent(metadata),
383-
Schema = new OpenApiSchema { Type = SchemaGenerator.GetOpenApiSchemaType(parameter.ParameterType) },
374+
Schema = OpenApiSchemaGenerator.GetOpenApiSchema(parameter.ParameterType),
384375
Required = !isOptional
385376

386377
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.OpenApi.Models;
7+
8+
namespace Microsoft.AspNetCore.OpenApi;
9+
10+
internal static class OpenApiSchemaGenerator
11+
{
12+
private static readonly Dictionary<Type, (string, string?)> simpleTypesAndFormats =
13+
new()
14+
{
15+
[typeof(bool)] = ("boolean", null),
16+
[typeof(byte)] = ("string", "byte"),
17+
[typeof(int)] = ("integer", "int32"),
18+
[typeof(uint)] = ("integer", "int32"),
19+
[typeof(ushort)] = ("integer", "int32"),
20+
[typeof(long)] = ("integer", "int64"),
21+
[typeof(ulong)] = ("integer", "int64"),
22+
[typeof(float)] = ("number", "float"),
23+
[typeof(double)] = ("number", "double"),
24+
[typeof(decimal)] = ("number", "double"),
25+
[typeof(DateTime)] = ("string", "date-time"),
26+
[typeof(DateTimeOffset)] = ("string", "date-time"),
27+
[typeof(TimeSpan)] = ("string", "date-span"),
28+
[typeof(Guid)] = ("string", "uuid"),
29+
[typeof(char)] = ("string", null),
30+
[typeof(Uri)] = ("string", "uri"),
31+
[typeof(string)] = ("string", null),
32+
[typeof(object)] = ("object", null)
33+
};
34+
35+
internal static OpenApiSchema GetOpenApiSchema(Type? type)
36+
{
37+
if (type is null)
38+
{
39+
return new OpenApiSchema();
40+
}
41+
42+
var (openApiType, openApiFormat) = GetTypeAndFormatProperties(type);
43+
return new OpenApiSchema
44+
{
45+
Type = openApiType,
46+
Format = openApiFormat,
47+
Nullable = Nullable.GetUnderlyingType(type) != null,
48+
};
49+
}
50+
51+
private static (string, string?) GetTypeAndFormatProperties(Type type)
52+
{
53+
type = Nullable.GetUnderlyingType(type) ?? type;
54+
55+
if (simpleTypesAndFormats.TryGetValue(type, out var typeAndFormat))
56+
{
57+
return typeAndFormat;
58+
}
59+
60+
if (type == typeof(IFormFileCollection) || type == typeof(IFormFile))
61+
{
62+
return ("object", null);
63+
}
64+
65+
if (typeof(IDictionary).IsAssignableFrom(type))
66+
{
67+
return ("object", null);
68+
}
69+
70+
if (type != typeof(string) && (type.IsArray || typeof(IEnumerable).IsAssignableFrom(type)))
71+
{
72+
return ("array", null);
73+
}
74+
75+
return ("object", null);
76+
}
77+
}

src/OpenApi/src/SchemaGenerator.cs

Lines changed: 0 additions & 44 deletions
This file was deleted.

src/OpenApi/test/OpenApiGeneratorTests.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public void AddsMultipleResponseFormatsFromMetadataWithPoco()
170170
var content = Assert.Single(createdResponseType.Content);
171171

172172
Assert.NotNull(createdResponseType);
173-
Assert.Equal("object", content.Value.Schema.Type);
173+
Assert.Equal("string", content.Value.Schema.Type);
174174
Assert.Equal("application/json", createdResponseType.Content.Keys.First());
175175

176176
var badRequestResponseType = responses["400"];
@@ -209,7 +209,7 @@ public void AddsFromRouteParameterAsPath()
209209
static void AssertPathParameter(OpenApiOperation operation)
210210
{
211211
var param = Assert.Single(operation.Parameters);
212-
Assert.Equal("number", param.Schema.Type);
212+
Assert.Equal("integer", param.Schema.Type);
213213
Assert.Equal(ParameterLocation.Path, param.In);
214214
}
215215

@@ -235,7 +235,7 @@ public void AddsFromRouteParameterAsPathWithNullablePrimitiveType()
235235
static void AssertPathParameter(OpenApiOperation operation)
236236
{
237237
var param = Assert.Single(operation.Parameters);
238-
Assert.Equal("number", param.Schema.Type);
238+
Assert.Equal("integer", param.Schema.Type);
239239
Assert.Equal(ParameterLocation.Path, param.In);
240240
}
241241

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

268-
AssertQueryParameter(GetOpenApiOperation((int foo) => { }, "/"), "number");
269-
AssertQueryParameter(GetOpenApiOperation(([FromQuery] int foo) => { }), "number");
268+
AssertQueryParameter(GetOpenApiOperation((int foo) => { }, "/"), "integer");
269+
AssertQueryParameter(GetOpenApiOperation(([FromQuery] int foo) => { }), "integer");
270270
AssertQueryParameter(GetOpenApiOperation(([FromQuery] TryParseStringRecordStruct foo) => { }), "object");
271271
AssertQueryParameter(GetOpenApiOperation((int[] foo) => { }, "/"), "array");
272272
AssertQueryParameter(GetOpenApiOperation((string[] foo) => { }, "/"), "array");
273-
AssertQueryParameter(GetOpenApiOperation((StringValues foo) => { }, "/"), "object");
273+
AssertQueryParameter(GetOpenApiOperation((StringValues foo) => { }, "/"), "array");
274274
AssertQueryParameter(GetOpenApiOperation((TryParseStringRecordStruct[] foo) => { }, "/"), "array");
275275
}
276276

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

300-
Assert.Equal("number", param.Schema.Type);
300+
Assert.Equal("integer", param.Schema.Type);
301301
Assert.Equal(ParameterLocation.Header, param.In);
302302
}
303303

@@ -325,7 +325,7 @@ static void AssertBodyParameter(OpenApiOperation operation, string expectedName,
325325
}
326326

327327
AssertBodyParameter(GetOpenApiOperation((InferredJsonClass foo) => { }), "foo", "object");
328-
AssertBodyParameter(GetOpenApiOperation(([FromBody] int bar) => { }), "bar", "number");
328+
AssertBodyParameter(GetOpenApiOperation(([FromBody] int bar) => { }), "bar", "integer");
329329
}
330330

331331
#nullable enable
@@ -338,13 +338,13 @@ public void AddsMultipleParameters()
338338

339339
var fooParam = operation.Parameters[0];
340340
Assert.Equal("foo", fooParam.Name);
341-
Assert.Equal("number", fooParam.Schema.Type);
341+
Assert.Equal("integer", fooParam.Schema.Type);
342342
Assert.Equal(ParameterLocation.Path, fooParam.In);
343343
Assert.True(fooParam.Required);
344344

345345
var barParam = operation.Parameters[1];
346346
Assert.Equal("bar", barParam.Name);
347-
Assert.Equal("number", barParam.Schema.Type);
347+
Assert.Equal("integer", barParam.Schema.Type);
348348
Assert.Equal(ParameterLocation.Query, barParam.In);
349349
Assert.True(barParam.Required);
350350

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

364364
var fooParam = operation.Parameters[0];
365365
Assert.Equal("foo", fooParam.Name);
366-
Assert.Equal("number", fooParam.Schema.Type);
366+
Assert.Equal("integer", fooParam.Schema.Type);
367367
Assert.Equal(ParameterLocation.Path, fooParam.In);
368368
Assert.True(fooParam.Required);
369369

370370
var barParam = operation.Parameters[1];
371371
Assert.Equal("bar", barParam.Name);
372-
Assert.Equal("number", barParam.Schema.Type);
372+
Assert.Equal("integer", barParam.Schema.Type);
373+
Assert.True(barParam.Schema.Nullable);
373374
Assert.Equal(ParameterLocation.Query, barParam.In);
374375
Assert.False(barParam.Required);
375376
}
@@ -388,7 +389,7 @@ public void TestParameterIsRequiredForObliviousNullabilityContext()
388389
Assert.False(fooParam.Required);
389390

390391
var barParam = operation.Parameters[1];
391-
Assert.Equal("number", barParam.Schema.Type);
392+
Assert.Equal("integer", barParam.Schema.Type);
392393
Assert.Equal(ParameterLocation.Query, barParam.In);
393394
Assert.True(barParam.Required);
394395
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Text.Json.Nodes;
6+
using Microsoft.AspNetCore.OpenApi;
7+
8+
namespace Microsoft.AspNetCore.OpenApi.Tests;
9+
10+
public class OpenApiSchemaGeneratorTests
11+
{
12+
[Theory]
13+
[InlineData(typeof(Dictionary<string, string>))]
14+
[InlineData(typeof(Todo))]
15+
public void CanGenerateCorrectSchemaForDictionaryTypes(Type type)
16+
{
17+
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(type);
18+
Assert.NotNull(schema);
19+
Assert.Equal("object", schema.Type);
20+
}
21+
22+
[Theory]
23+
[InlineData(typeof(IList<string>))]
24+
[InlineData(typeof(Products))]
25+
public void CanGenerateSchemaForListTypes(Type type)
26+
{
27+
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(type);
28+
Assert.NotNull(schema);
29+
Assert.Equal("array", schema.Type);
30+
}
31+
32+
[Theory]
33+
[InlineData(typeof(DateTime))]
34+
[InlineData(typeof(DateTimeOffset))]
35+
public void CanGenerateSchemaForDateTimeTypes(Type type)
36+
{
37+
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(type);
38+
Assert.NotNull(schema);
39+
Assert.Equal("string", schema.Type);
40+
Assert.Equal("date-time", schema.Format);
41+
}
42+
43+
[Fact]
44+
public void CanGenerateSchemaForDateSpanTypes()
45+
{
46+
var schema = OpenApiSchemaGenerator.GetOpenApiSchema(typeof(TimeSpan));
47+
Assert.NotNull(schema);
48+
Assert.Equal("string", schema.Type);
49+
Assert.Equal("date-span", schema.Format);
50+
}
51+
52+
class Todo : Dictionary<string, object> { }
53+
class Products : IList<int>
54+
{
55+
public int this[int index] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
56+
57+
public int Count => throw new NotImplementedException();
58+
59+
public bool IsReadOnly => throw new NotImplementedException();
60+
61+
public void Add(int item)
62+
{
63+
throw new NotImplementedException();
64+
}
65+
66+
public void Clear()
67+
{
68+
throw new NotImplementedException();
69+
}
70+
71+
public bool Contains(int item)
72+
{
73+
throw new NotImplementedException();
74+
}
75+
76+
public void CopyTo(int[] array, int arrayIndex)
77+
{
78+
throw new NotImplementedException();
79+
}
80+
81+
public IEnumerator<int> GetEnumerator()
82+
{
83+
throw new NotImplementedException();
84+
}
85+
86+
public int IndexOf(int item)
87+
{
88+
throw new NotImplementedException();
89+
}
90+
91+
public void Insert(int index, int item)
92+
{
93+
throw new NotImplementedException();
94+
}
95+
96+
public bool Remove(int item)
97+
{
98+
throw new NotImplementedException();
99+
}
100+
101+
public void RemoveAt(int index)
102+
{
103+
throw new NotImplementedException();
104+
}
105+
106+
IEnumerator IEnumerable.GetEnumerator()
107+
{
108+
throw new NotImplementedException();
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)