Skip to content

TimeSpan is (de)serialized incorrectly using the JSON source generator #62082

Closed
@martincostello

Description

@martincostello

Description

If a TimeSpan is serialized using the JSON source generator it is output as an object:

{"duration":{"days":0,"hours":0,"milliseconds":0,"minutes":0,"seconds":1,"ticks":10000000,"totalDays":1.1574074074074073E-05,"totalHours":0.0002777777777777778,"totalMilliseconds":1000,"totalMinutes":0.016666666666666666,"totalSeconds":1}}

If a TimeSpan is serialized without using the source generator, it is serialized as a string:

{"duration":"00:00:01"}

Similarly, the source generator will fail to deserialize strings to TimeSpan values as it is expecting an object.

Curiously, if the instance of JsonSerializerOptions used to serialize as a string is passed to a new instance of the custom JSON serializer context but then not used, then same operation will then change to serializing as an object from a string.

Reproduction Steps

To reproduce run dotnet build using the .NET 6.0.100 SDK for the below application.

using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

var value = new MyObject {  Duration = TimeSpan.FromSeconds(1) };

var context = new MyJsonContext(new(JsonSerializerDefaults.Web));
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

// Outputs the TimeSpan as an object
var bytes = JsonSerializer.SerializeToUtf8Bytes(value, typeof(MyObject), context);
var json = Encoding.UTF8.GetString(bytes);

Console.WriteLine(json);
Console.WriteLine();

// Outputs the TimeSpan as a string
bytes = JsonSerializer.SerializeToUtf8Bytes<MyObject>(value, options);
json = Encoding.UTF8.GetString(bytes);

Console.WriteLine(json);
Console.WriteLine();

options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

// Create a new context that uses the same options.
// This causes the behavior of SerializeToUtf8Bytes<T>() to change.
context = new MyJsonContext(options);

// Outputs the TimeSpan as an object again
bytes = JsonSerializer.SerializeToUtf8Bytes<MyObject>(value, options);
json = Encoding.UTF8.GetString(bytes);

Console.WriteLine(json);
Console.WriteLine();

try
{
    JsonSerializer.Deserialize<MyObject>("{\"duration\":\"00:00:01\"}", options);
}
catch (JsonException ex)
{
    Console.Error.WriteLine("Failed to deserialize TimeSpan from string: {0}", ex);
    Console.Error.WriteLine();
}

[JsonSerializable(typeof(MyObject))]
public partial class MyJsonContext : JsonSerializerContext
{
}

public class MyObject
{
    public TimeSpan Duration { get; set; }
}
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <ImplicitUsings>enable</ImplicitUsings>
    <NoWarn>$(NoWarn);CA1050</NoWarn>
    <Nullable>enable</Nullable>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>
</Project>

Expected behavior

The application outputs the following:

{"duration":"00:00:01"}
{"duration":"00:00:01"}
{"duration":"00:00:01"}

Actual behavior

The application outputs the following:

{"duration":{"days":0,"hours":0,"milliseconds":0,"minutes":0,"seconds":1,"ticks":10000000,"totalDays":1.1574074074074073E-05,"totalHours":0.0002777777777777778,"totalMilliseconds":1000,"totalMinutes":0.016666666666666666,"totalSeconds":1}}

{"duration":"00:00:01"}

{"duration":{"days":0,"hours":0,"milliseconds":0,"minutes":0,"seconds":1,"ticks":10000000,"totalDays":1.1574074074074073E-05,"totalHours":0.0002777777777777778,"totalMilliseconds":1000,"totalMinutes":0.016666666666666666,"totalSeconds":1}}

Failed to deserialize TimeSpan from string: System.Text.Json.JsonException: The JSON value could not be converted to System.TimeSpan. Path: $.duration | LineNumber: 0 | BytePositionInLine: 22.
   at System.Text.Json.ThrowHelper.ThrowJsonException_DeserializeUnableToConvertValue(Type propertyType)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.Converters.JsonMetadataServicesConverter`1.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)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.Converters.JsonMetadataServicesConverter`1.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)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Program.<Main>$(String[] args) in C:\Coding\martincostello\JsonSourceGeneratorObsoleteProperties\Program.cs:line 39

Regression?

No.

Known Workarounds

No response

Configuration

Partial output from dotnet --info:

> dotnet --info
.NET SDK (reflecting any global.json):
 Version:   6.0.100
 Commit:    9e8b04bbff

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.19043
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\6.0.100\

Host (useful for support):
  Version: 6.0.0
  Commit:  4822e3c3aa

Other information

No response

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions