Why System.Version in JSON string does not deserialize correctly?
Asked Answered
C

3

25

Context: I need to pass an object containing a large number of properties/fields (to UI Layer from Middle Tier Layer). Among this list of properties, one is of type Version which is not getting deserialized correctly from JSON string format. I have a chosen JSON format over XML as JSON serialization to string will return short string result.

Problem: System.Version does not get deserialized correctly. I have tried two different .NET Libraries. Following are the code snippets for each:

Code Snippet 1 using ServiceStack .NET library:

        var version = new Version(1, 2, 3, 0);
        string reportJSON = JsonSerializer.SerializeToString<Version>(version);
        //{"Major":1,"Minor":2,"Build":3,"Revision":0,"MajorRevision":0,"MinorRevision":0}


        Version report2 = JsonSerializer.DeserializeFromString<Version>(reportJSON);
        string reportJSON2 = JsonSerializer.SerializeToString<Version>(report2);
        //{"Major":0,"Minor":0,"Build":-1,"Revision":-1,"MajorRevision":-1,"MinorRevision":-1}

Code Snippet 2 using Newtonsoft .NET library but with same result:

        var version = new Version(1, 2, 3, 0);
        string reportJSON = JsonConvert.SerializeObject(version);
        //{"Major":1,"Minor":2,"Build":3,"Revision":0,"MajorRevision":0,"MinorRevision":0}


        Version report2 = JsonConvert.DeserializeObject<Version>(reportJSON);
        string reportJSON2 = JsonConvert.SerializeObject(report2);
        //{"Major":0,"Minor":0,"Build":-1,"Revision":-1,"MajorRevision":-1,"MinorRevision":-1}

How to fix this? Or Which other JSON.NET library can work correctly?

Caressive answered 1/11, 2012 at 3:6 Comment(3)
possible duplicate of Can I deserialize to an immutable object using JavascriptSerializer?Glyn
@spender: but it works fine with string objects which are immutable.Caressive
@Monish Here is a link that explains why the Version class is not XML-serializable, but I'm pretty sure same mechanism applies to JSON serializers.Ilowell
L
31

The Newtonsoft.Json library provides a set of common converters in the Newtonsoft.Json.Convertersnamespace, including a VersionConverter you can use to serialize and deserialize System.Version.

Note that you have to use the VersionConverterboth for serialization and deserialization, though.
That's because standard serialization would generate eg.:{"Major":1,"Minor":2,"Build":3,"Revision":0,"MajorRevision":0,"MinorRevision":0} while VersionConverterdeserialization expects a simple string as in "1.2.3".

So usage would be:

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;  

string s = JsonConvert.SerializeObject(version, new VersionConverter());
Version v = JsonConvert.DeserializeObject<Version>(s, new VersionConverter());

I'm not sure what's the first version of Newtonsoft.Jsonthat includes that converter. Mine has it and it's 5.0.6.

Loquitur answered 9/9, 2013 at 21:51 Comment(1)
Worked for me, only I had to make a little change since I'm using the DeserializeObject signature that gets the JsonSerializerSettings object: settings.Converters.Add(new VersionConverter()); (no overload takes both settings and a collection of converters)Subdual
M
13

The properties of the Version class have no setter. They just return the value of their corresponding private fields. Therefore, the deserializer is not able to change their values.

But with Json.NET you can write a custom converter class which handles the deserialization of the Version class.

Beware: This code has not been tested very well...

public class VersionConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // default serialization
        serializer.Serialize(writer, value);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // create a new Version instance and pass the properties to the constructor
        // (you may also use dynamics if you like)
        var dict = serializer.Deserialize<Dictionary<string, int>>(reader);
        return new Version(dict["Major"], dict["Minor"], dict["Build"], dict["Revision"]);
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Version);
    }
}

Then you have to specify that you want to use the converter:

var v = new Version(1, 2, 3, 4);
string json = JsonConvert.SerializeObject(v);

var v2 = JsonConvert.DeserializeObject<Version>(json, new VersionConverter());

Json.NET decides itself whether to use one of the converters you specified. So you can always specify the converter, as shown below. Json.NET will use one of your converters if they match a type within SomeClass.

var result = JsonConvert.DeserializeObject<SomeClass>(json, new VersionConverter());
Mareah answered 1/11, 2012 at 12:43 Comment(5)
What if I have a class called Userinfo and inside the class there is a variable like Dictionary<Version, string>? This var result = JsonConvert.DeserializeObject<Userinfo>(json, new VersionConverter()); doesn't workEimile
With Json.NET 11 (maybe also earlier versions), you can just serialize/deserialize a Version object with the default serializer as it doesn't result in an object with Major and Minor properties etc. anymore. The default serialization result of a Version is now a string like "1.2.3.4". And deserialization works fine. Hence using it as a key in a dictionary is not a problem. (I just tested it and it works perfectly.)Mareah
Please note that this will not work for objects with only some parts set e.g. 2.0. The oter parts are serialised as -1 which then causes an exception at new Version(...)Griffy
So they changed the way it's serialized, and also what it expects to be able to deserialize, with no respect for backward compatibility. Thus breaking anything in existence that was already serializing a Version object the old way. Wonderful.Powwow
To clarify: As of may 2021, NET Framework builds with newtonsoft 13 can deserialize from either format. Dotnet core builds with newtonsoft 13 can only deserialize from string format.Powwow
G
1

As of Json.NET version 13 and possibly earlier, Json.NET will now round-trip System.Version without need for a converter, however its default serialization is inconsistent between .NET Core and .NET Framework.

  • In .NET Core 6 and later it is automatically round-tripped as a JSON string using its ToString() representation (here "1.2.3.0").

    Demo fiddle #1 here.

    System.Text.Json has also serialized Version identically since at least .NET Core 6.

  • In .NET Framework 4.7.2 it is automatically round-tripped as a JSON object by default (here {"Major":1,"Minor":2,"Build":3,"Revision":0,"MajorRevision":0,"MinorRevision":0})

    Demo fiddle #2 here.

    However, as noted by Manuel in this answer, you can use VersionConverter to force Version to be round-tripped as a string.

See: JToken.FromObject(System.Version) behaves differently from dotnet core and framework #2588.

If for some reason you are dealing with JSON that might contain a Version serialized as either an object or a string, you will need to write your own JsonConverter<Version> that handles this. The following is one such:

public class VersionObjectConverter : JsonConverter<Version>
{
    public VersionObjectConverter() : this(false) { } // Serialize as a string by default
    public VersionObjectConverter(bool serializeAsObject) { this.SerializeAsObject = serializeAsObject; }

    static readonly Newtonsoft.Json.Converters.VersionConverter InnerConverter = new Newtonsoft.Json.Converters.VersionConverter();
    bool SerializeAsObject { get; set; }

    class VersionDto
    {
        public int Major; public int Minor; public int Build = -1; public int Revision = -1; public Int16 MajorRevision = -1; public Int16 MinorRevision = -1;
        public Version ToVersion()
        {
            if (Build == -1)
                return new Version(Major, Minor);
            else if (Revision == -1)
                return new Version(Major, Minor, Build);
            else
                return new Version(Major, Minor, Build, Revision);
        }
    }

    public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer)
    {
        if (value == null)
            writer.WriteNull();
        else if (SerializeAsObject)
            serializer.Serialize(writer, new VersionDto { Major = value.Major, Minor = value.Minor, Build = value.Build, Revision = value.Revision, MajorRevision = value.MajorRevision, MinorRevision = value.MinorRevision});
        else
            InnerConverter.WriteJson(writer, value, serializer);
    }

    public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        switch (reader.MoveToContentAndAssert().TokenType)
        {
            case JsonToken.Null: return null;
            case JsonToken.String: return (Version)InnerConverter.ReadJson(reader, objectType, hasExistingValue ? existingValue : null, serializer);
            case JsonToken.StartObject: return serializer.Deserialize<VersionDto>(reader).ToVersion();
            default: throw new JsonSerializationException(string.Format("Unknown token type {0}", reader.TokenType));
        }
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException("reader");
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException("reader");
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

And round-trip your Version as follows:

var settings = new JsonSerializerSettings
{
    Converters = { new VersionObjectConverter(serializeAsObject : true) }, // Use serializeAsObject : false if you don't want Version to be serialized as a JSON object
};

var reportJSON = JsonConvert.SerializeObject(version, settings); 
var report2 = JsonConvert.DeserializeObject<Version>(reportJSON, settings);
var reportJSON2 = JsonConvert.SerializeObject(report2, settings);

Assert.That(report2 == version); // Passes
Assert.That(reportJSON == "{\"Major\":1,\"Minor\":2,\"Build\":3,\"Revision\":0,\"MajorRevision\":0,\"MinorRevision\":0}"); // Passes

Notes:

  • The converter is written in using .NET Framework 4.7.2 / C# 7.3 syntax.

  • To control whether Version is re-serialized as a string or an object, pass serializeAsObject : true or serializeAsObject : false to the constructor.

  • When Revision is -1, a Version constructor that does not take the revision must be used. And when Build is -1 the constructor that takes neither must be used. VersionConverter from this answer does not handle this correctly.

Demo fiddles #3 for .NET Framework 4.7.2 here and for #4 for .NET Core 6 here.

Gaiseric answered 27/5 at 16:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.