aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

TimeSpan shown as object in OpenAPI definition is not compatible with System.Text.Json serialization

Open christiannagel opened this issue 11 months ago • 0 comments

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.SmallObjectWithParameterizedConstructorConverter5.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.ObjectWithParameterizedConstructorConverter1.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.JsonConverter1.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.JsonConverter1.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.JsonTypeInfo1.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

christiannagel avatar Mar 13 '24 12:03 christiannagel