.Net Core 3.0 TimeSpan deserialization error - Fixed in .Net 5.0
Asked Answered
C

3

24

I am using .Net Core 3.0 and have the following string which I need to deserialize with Newtonsoft.Json:

{
    "userId": null,
    "accessToken": null,
    "refreshToken": null,
    "sessionId": null,
    "cookieExpireTimeSpan": {
        "ticks": 0,
        "days": 0,
        "hours": 0,
        "milliseconds": 0,
        "minutes": 0,
        "seconds": 0,
        "totalDays": 0,
        "totalHours": 0,
        "totalMilliseconds": 0,
        "totalMinutes": 0,
        "totalSeconds": 0
    },
    "claims": null,
    "success": false,
    "errors": [
        {
            "code": "Forbidden",
            "description": "Invalid username unknown!"
        }
    ]
}

and bump into the following error:

   Newtonsoft.Json.JsonSerializationException : Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.TimeSpan' because the type requires a JSON primitive value (e.g. string, number, boolean, null) to deserialize correctly.
To fix this error either change the JSON to a JSON primitive value (e.g. string, number, boolean, null) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List<T>) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.
Path 'cookieExpireTimeSpan.ticks', line 1, position 103.

The error string actually happens when reading the content of HttpResponseMessage:

var httpResponse = await _client.PostAsync("/api/auth/login", new StringContent(JsonConvert.SerializeObject(new API.Models.Request.LoginRequest()), Encoding.UTF8, "application/json"));
var stringResponse = await httpResponse.Content.ReadAsStringAsync();

The server controller method returns:

return new JsonResult(result) { StatusCode = whatever; };
Claro answered 8/10, 2019 at 9:35 Comment(7)
There's no Timespan value in that JSON. A TimeSpan is a time value, eg 12:00Mob
That's the response I get from a WebAPI. The only "valid" value in that object is "success: false". The other values are just "default" values.Claro
In that case ask the Web API's author to fix it the bug. JSON.NET won't generate such a string.Mob
It's my web api. I didn't have this error when i was using .Net Core 2.2. Only see this after I have upgraded to .Net Core 3.0 recently and all my integration tests fail due to that. What do I miss?Claro
I suspect that the WebApi has changed in the time when you upgraded to 3.0Surprisal
Could you post an example response without the authentication error?Surprisal
In the success code execution path, the login controller actually sets the cookieExpireTimeSpan. So the serialized string is "cookieExpireTimeSpan": "00:00:00". I am lost now as to where goes wrong. Serialization or Deserialization? I can't fix what I don't know where goes wrong.Claro
M
79

The REST API service shouldn't produce such a JSON string. I'd bet that previous versions returned 00:0:00 instead of all the properties of a TimeSpan object.

The reason for this is that .NET Core 3.0 replaced JSON.NET with a new, bult-in JSON serializer, System.Text.Json. This serializer doesn't support TimeSpan. The new serializer is faster, doesn't allocate in most cases, but doesn't cover all the cases JSON.NET did.

In any case, there's no standard way to represent dates or periods in JSON. Even the ISO8601 format is a convention, not part of the standard itself. JSON.NET uses a readable format (23:00:00), but ISO8601's duration format would look like P23DT23H (23 days, 23 hours) or P4Y (4 years).

One solution is to go back to JSON.NET. The steps are described in the docs:

services.AddMvc()
    .AddNewtonsoftJson();

Another option is to use a custom converter for that type, eg :

public class TimeSpanToStringConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value=reader.GetString();
        return TimeSpan.Parse(value);
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }
}

And register it in Startup.ConfigureServices with AddJsonOptions, eg :

services.AddControllers()                    
        .AddJsonOptions(options=>
            options.JsonSerializerOptions.Converters.Add(new TimeSpanToStringConverter()));

Mob answered 8/10, 2019 at 9:57 Comment(12)
How not to "go back" but fix the issues? Where / what to fix?Claro
@KokHowTeh added how to create and register a custom converterMob
the error actually appens at the client side when reading the HttpReponseMessage. I have updated my original post.Claro
@KokHowTeh the problem is caused by the server serializing a TimeSpan as if it were an object instead of a duration. You need to fix the server, not the client.Mob
Is this a bug that I should report to the .net core git as an issue for them to fix?Claro
I already posted a link to the Github issue that explains why this happened and that there are no plans to change this right now. You need to add the custom converter to your application or go back to Json.NETMob
@KokHowTeh in fact, I even trolled Immo Landwerth on Twitter about itMob
That works. Hope they fix them in the near future. Thanks!Claro
In TimeSpanToStringConverter I recommend parsing and formatting in the invariant culture, i.e. TimeSpan.Parse(value, CultureInfo.InvariantCulture) and ((TimeSpan)value).ToString(null, CultureInfo.InvariantCulture)Sacrum
How did this pass review? :O This makes apps silently fail/return wrong values.Bipod
TimeSpanJsonConverter seems like a better name than TimeSpanToStringConverter.Institutive
+1 for great answer and background detail, thanks! I'm just adding this here for those like me hitting both this and the same issue for System.Version. It seems this exact issue exists where no VersionConverter exists and according to this it has already been added/fixed for .NET 6.0. For 5.0 you'll have to do basically the same as in this answer, but for System.Version.Aludel
C
6

My solution is to use custom converter, but with explicitly specified standard non culture-sensitive TimeSpan format specifier .

public class JsonTimeSpanConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return TimeSpan.ParseExact(reader.GetString(), "c", CultureInfo.InvariantCulture);
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString("c", CultureInfo.InvariantCulture));
    }
}

Then register it in the Startup for the HostBuilder:

public class Startup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        ...
        services
            ...
            .AddJsonOptions(opts =>
            {
                opts.JsonSerializerOptions.Converters.Add(new JsonTimeSpanConverter());                    
            });
        ...
    }
}
Chromolithography answered 2/2, 2021 at 10:33 Comment(1)
When using the "c" format specifier, you don't need to specify CultureInfo.InvariantCulture (the format is already culture-invariant see docs). In fact "c" is the default value used by ToString() so you can simply write writer.WriteStringValue(value.ToString());Dialysis
H
0

TimeSpanConverter is available in .NET 6.0. So TimeSpan serialization/deserialization will work without custom converters out of the box.

Issue: https://github.com/dotnet/runtime/issues/29932

Implementation: https://github.com/dotnet/runtime/pull/54186

Hanshaw answered 1/10, 2021 at 17:13 Comment(2)
How about an example of it working?Bron
I don't think this is necessary for obvious code. But I agree with you that we can't believe everything that is written on the Internet and need to check everything)) dotnetfiddle.net/13kIUoHanshaw

© 2022 - 2024 — McMap. All rights reserved.