aspnetcore
aspnetcore copied to clipboard
TimeSpan shown as object in OpenAPI definition is not compatible with System.Text.Json serialization
Is there an existing issue for this?
- [X] I have searched the existing issues
Describe the bug
Creating a minimal APIs project using an object containing a TimeSpan
such as
public record class Test(Guid Id, TimeSpan Duration);
Creates this OpenAPI definition:
"schemas": {
"Test": {
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"duration": {
"$ref": "#/components/schemas/TimeSpan"
}
},
"additionalProperties": false
},
"TimeSpan": {
"type": "object",
"properties": {
"ticks": {
"type": "integer",
"format": "int64"
},
"days": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"hours": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"milliseconds": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"microseconds": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"nanoseconds": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"minutes": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"seconds": {
"type": "integer",
"format": "int32",
"readOnly": true
},
"totalDays": {
"type": "number",
"format": "double",
"readOnly": true
},
"totalHours": {
"type": "number",
"format": "double",
"readOnly": true
},
"totalMilliseconds": {
"type": "number",
"format": "double",
"readOnly": true
},
"totalMicroseconds": {
"type": "number",
"format": "double",
"readOnly": true
},
"totalNanoseconds": {
"type": "number",
"format": "double",
"readOnly": true
},
"totalMinutes": {
"type": "number",
"format": "double",
"readOnly": true
},
"totalSeconds": {
"type": "number",
"format": "double",
"readOnly": true
}
},
"additionalProperties": false
}
}
Sending a POST request passing this information fails with a BadHttpRequestException
because the System.Text.Json does not expect an object.
Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to read parameter "Test test" from the request body as JSON.
---> System.Text.Json.JsonException: The JSON value could not be converted to Test. Path: $.duration | LineNumber: 2 | BytePositionInLine: 15.
---> System.InvalidOperationException: Cannot get the value of a token type 'StartObject' as a string.
Expected Behavior
I would expect this to run. Having an HTTP client calling the API succeeds, just the OpenAPI document that's created is not compatible and does not succeed calling the API. Same behavior when the JSON source generator is used.
Steps To Reproduce
Create a Web API project (see this repo: https://github.com/christiannagel/issues-timestamp)
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/test", () =>
{
return new Test(Guid.NewGuid(), TimeSpan.FromSeconds(10));
});
app.MapPost("/test", (Test test) =>
{
Console.WriteLine(test.Id);
Console.WriteLine(test.Duration);
return Results.Ok();
});
app.Run();
public record class Test(Guid Id, TimeSpan Duration);
Calling the API with a .NET client (also part of the source code repo) succeeds. The issue is with the Swagger OpenAPI creation, and might be an issue with https://github.com/domaindrivendev/Swashbuckle.AspNetCore or https://github.com/microsoft/OpenAPI.NET
Exceptions (if any)
Exception:
Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to read parameter "Test test" from the request body as JSON.
---> System.Text.Json.JsonException: The JSON value could not be converted to Test. Path: $.duration | LineNumber: 2 | BytePositionInLine: 15.
---> System.InvalidOperationException: Cannot get the value of a token type 'StartObject' as a string.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_ExpectedString(JsonTokenType tokenType)
at System.Text.Json.Serialization.Converters.TimeSpanConverter.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
at System.Text.Json.Serialization.JsonConverter1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue) at System.Text.Json.Serialization.Converters.SmallObjectWithParameterizedConstructorConverter
5.TryRead[TArg](ReadStack& state, Utf8JsonReader& reader, JsonParameterInfo jsonParameterInfo, TArg& arg)
at System.Text.Json.Serialization.Converters.SmallObjectWithParameterizedConstructorConverter5.ReadAndCacheConstructorArgument(ReadStack& state, Utf8JsonReader& reader, JsonParameterInfo jsonParameterInfo) at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter
1.ReadConstructorArgumentsWithContinuation(ReadStack& state, Utf8JsonReader& reader, JsonSerializerOptions options)
at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value) at System.Text.Json.Serialization.JsonConverter
1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)
at System.Text.Json.Serialization.JsonConverter1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state) --- End of inner exception stack trace --- at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, Utf8JsonReader& reader, Exception ex) at System.Text.Json.Serialization.JsonConverter
1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo1.ContinueDeserialize(ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack) at System.Text.Json.Serialization.Metadata.JsonTypeInfo
1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.DeserializeAsObjectAsync(Stream utf8Json, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync(HttpRequest request, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<HandleRequestBodyAndCompileRequestDelegateForJson>g__TryReadBodyAsync|102_0(HttpContext httpContext, Type bodyType, String parameterTypeName, String parameterName, Boolean allowEmptyRequestBody, Boolean throwOnBadRequest, JsonTypeInfo jsonTypeInfo)
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Http.RequestDelegateFactory.Log.InvalidJsonRequestBody(HttpContext httpContext, String parameterTypeName, String parameterName, Exception exception, Boolean shouldThrow)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<HandleRequestBodyAndCompileRequestDelegateForJson>g__TryReadBodyAsync|102_0(HttpContext httpContext, Type bodyType, String parameterTypeName, String parameterName, Boolean allowEmptyRequestBody, Boolean throwOnBadRequest, JsonTypeInfo jsonTypeInfo)
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<>c__DisplayClass102_2.<<HandleRequestBodyAndCompileRequestDelegateForJson>b__2>d.MoveNext()
--- End of stack trace from previous location ---
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
.NET Version
8.0.200
Anything else?
.NET SDK: Version: 8.0.200 Commit: 438cab6a9d Workload version: 8.0.200-manifests.5295d9b5
Runtime Environment: OS Name: Windows OS Version: 10.0.22631 OS Platform: Windows RID: win-x64 Base Path: C:\Program Files\dotnet\sdk\8.0.200\
.NET workloads installed: [aspire] Installation Source: SDK 8.0.200 Manifest Version: 8.0.0-preview.5.24162.5/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.aspire\8.0.0-preview.5.24162.5\WorkloadManifest.json Install Type: FileBased
[maui-windows] Installation Source: VS 17.9.34622.214, VS 17.10.34707.107 Manifest Version: 8.0.10-ci.net8.10300/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maui\8.0.10-ci.net8.10300\WorkloadManifest.json Install Type: FileBased
[android] Installation Source: VS 17.9.34622.214, VS 17.10.34707.107 Manifest Version: 34.0.91/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.android\34.0.91\WorkloadManifest.json Install Type: FileBased
[ios] Installation Source: VS 17.9.34622.214, VS 17.10.34707.107 Manifest Version: 17.2.8257-ci.main/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.ios\17.2.8257-ci.main\WorkloadManifest.json Install Type: FileBased
[maccatalyst] Installation Source: VS 17.9.34622.214, VS 17.10.34707.107 Manifest Version: 17.2.8257-ci.main/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.sdk.maccatalyst\17.2.8257-ci.main\WorkloadManifest.json Install Type: FileBased
[wasm-tools-net7] Installation Source: VS 17.9.34622.214 Manifest Version: 8.0.3/8.0.100 Manifest Path: C:\Program Files\dotnet\sdk-manifests\8.0.100\microsoft.net.workload.mono.toolchain.net7\8.0.3\WorkloadManifest.json Install Type: FileBased
Host: Version: 8.0.2 Architecture: x64 Commit: 1381d5ebd2
.NET SDKs installed: 8.0.200 [C:\Program Files\dotnet\sdk]
.NET runtimes installed: Microsoft.AspNetCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 6.0.27 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.16 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 8.0.2 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
Other architectures found: x86 [C:\Program Files (x86)\dotnet] registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]
Environment variables: Not set
global.json file: Not found
Learn more: https://aka.ms/dotnet/info
Download .NET: https://aka.ms/dotnet/download