Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net Core 3.0 TimeSpan deserialization error - Fixed in .Net 5.0

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; };
like image 854
Kok How Teh Avatar asked Oct 08 '19 09:10

Kok How Teh


2 Answers

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:

  • Add a package reference to Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  • Update Startup.ConfigureServices to call AddNewtonsoftJson.

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()));

like image 93
Panagiotis Kanavos Avatar answered Sep 18 '22 16:09

Panagiotis Kanavos


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());                    
            });
        ...
    }
}
like image 34
kalitsov Avatar answered Sep 19 '22 16:09

kalitsov