7

The following program results in different outcomes when run on .NET 5 (or .NET Core) and .NET Framework.

Why is the behaviour different? I'm not after the solution to the deserialisation problem; my goal here is to understand what happens.

class Versioned 
{
    public Version V {get; set;} = new Version(1,0);
}

static void Main(string[] args)
{
    // Serialised with version Newtonsoft.Json 9.0.1
    var json = "{\"V\":{\"Major\":2,\"Minor\":0,\"Build\":-1,\"Revision\":-1,\"MajorRevision\":-1,\"MinorRevision\":-1}}";

    Console.WriteLine($".NET: {System.Environment.Version}");
    Console.WriteLine($"Json.NET: {System.Reflection.Assembly.GetAssembly(typeof(Newtonsoft.Json.JsonConvert))}");

    Console.WriteLine(json);
    
    try 
    {
        var b = Newtonsoft.Json.JsonConvert.DeserializeObject<Versioned>(json);
        Console.WriteLine(b.V);
    } 
    catch (Exception ex) { Console.WriteLine(ex.GetBaseException().Message); }
}

On .NET 5: the output is (formatted for readability):

.NET: 5.0.1
Json.NET: Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed
{
    "V": {
        "Major": 2,
        "Minor": 0,
        "Build": -1,
        "Revision": -1,
        "MajorRevision": -1,
        "MinorRevision": -1
    }
}

With this error:

Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Version' because the type requires a JSON string value to deserialize correctly.

To fix this error either change the JSON to a JSON string value 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) 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 'V.Major', line 1, position 14.

On .NET Framework 4.6.2 the output is:

.NET: 4.0.30319.42000
Json.NET: Newtonsoft.Json, Version=11.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed
{
    "V": {
        "Major": 2,
        "Minor": 0,
        "Build": -1,
        "Revision": -1,
        "MajorRevision": -1,
        "MinorRevision": -1
    }
}
1.0

Additionally, with NO default value the behaviour on .NET 4.6.2 is different again.

class Versioned
{
    public Version V { get; set; }// = new Version(1, 0);
}

On .NET 5.0 the output is the same:

(...)
Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Version' because...

On .NET Framework 4.6.2 the output now is:

(...)
Version's parameters must be greater than or equal to zero.
Parameter name: build

As far as I know, the System.Version class has not changed (on the outside) between these .NET versions in a way that should play a part here (I know only of the ISpanFormattable difference). I had a peek at the source (.NET Core version and .NET Framework 4.8 version) but I cannot see anything that would explain the different behaviours.

4
  • 4
    What happens if you update Newtonsoft.Json from 11 version to the latest one? Commented Jan 6, 2021 at 11:39
  • The behavior with the latest version of Json.NET (12.0.3) is the same as for v11. Commented Jan 6, 2021 at 11:51
  • 1
    I'm not sure how that ever worked... Based on the fact that the .NET Framework version didn't throw, I'm assuming it used reflection to find those backing fields, and did some fancy mapping to remove the leading _. Note that those backing fields have changed to readonly in .NET 5: that won't necesarily stop reflection, but perhaps it's enough to make json.net give up. Can you try on .NET 5 with your own Version class, which has non-readonly backing fields? Commented Jan 6, 2021 at 12:28
  • Interestingly, a custom version of Version with readonly fields also works: dotnetfiddle.net/tPP3rv . I removed the ToString stuff, but that shouldn't affect anything..... Commented Jan 6, 2021 at 12:41

1 Answer 1

7

It looks like Json.NET got a new VersionConverter in .NET Core 2.2, which knows how to properly serialize and deserialize Version instances. This automatically gets picked up and used when you're using .NET Core 2.2+.

Using VersionConverter, Json.NET wants to serialize your Version objects to strings like "1.0", rather than to JSON objects. If you create the json string in your post by serializing a new instance of Versioned on .NET Core 2.2+:

var json = Newtonsoft.Json.JsonConvert.SerializeObject(new Versioned());

You'll see that it returns e.g. {"V":"1.0"}.

Likewise, VersionConverter only knows how to read version strings such as "1.0", and doesn't know what to do with an object containing Major, Minor, etc.

See this issue for more. See code running here.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for looking at it. I tried to find the old code that did not throw at github.com/JamesNK/Newtonsoft.Json/commits/master/Src/… but I cannot see how it worked.
The old code is just the default JSON (de)serializer I think -- it didn't do anything special for Version, it just serialized its properties, and deserialized by using reflection to write to its private fields. The breaking change was to add VersionConverter

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.