9

Using Json.net, deserializing a type that contains a Tuple<...> doesn't work (serialization works, but deserialization doesn't):

[TestMethod]
public void Test()
{
    var orig = new TupleHolder("what????", true);
    var json = JsonConvert.SerializeObject(orig);
    Assert.AreEqual("{\"Tup\":{\"Item1\":\"what????\",\"Item2\":true}}", json);
    // great! serialization works like a charm! now let's test deserialization:
    var dupl = JsonConvert.DeserializeObject<TupleHolder>(json);

    Assert.AreEqual("ZZZ", dupl.Tup.Item1); // pass! but it should be "what????"... what????
    Assert.AreEqual(false, dupl.Tup.Item2); // pass! but it should be "true", right???

    Assert.AreEqual(orig.Tup.Item1, dupl.Tup.Item1); // fail!
    Assert.AreEqual(orig.Tup.Item2, dupl.Tup.Item2); // fail!
}

public class TupleHolder
{
    public Tuple<string, bool> Tup { get; set; }
    public TupleHolder() { Tup = new Tuple<string, bool>("ZZZ", false); }
    public TupleHolder(string s, bool b) { Tup = new Tuple<string, bool>(s, b); }
}

Funny thing is that direct deserialization of Tuple<...> does work:

[TestMethod]
public void Test2()
{
    var orig = new Tuple<string, bool>("ABC", true);
    var json = JsonConvert.SerializeObject(orig);
    var dupl = JsonConvert.DeserializeObject<Tuple<string, bool>>(json);
    Assert.AreEqual(orig, dupl); // direct deserialization of Tuple<...> works.
}

Is it a Json.NET bug or am I missing here something?

Tar
  • 8,529
  • 9
  • 56
  • 127
  • 2
    I think this is similar to your issue... http://stackoverflow.com/questions/23017716/json-net-how-to-deserialize-without-using-the-default-constructor . It's due to the parameterless constructor being used – Joel Gregory Jan 12 '15 at 09:39
  • @JoelGregory, somewhat... the thing is that if I had a `List` it would've been deserialized correctly, just `Tuple<...>` is problematic. I suppose that `Json.NET` checks if properties/fields implement `IEnumerable`/`ICollection`/`IList`/... and `Tuple<...>` doesn't... – Tar Jan 12 '15 at 09:48
  • @JoelGregory, a workaround could be to use `[JsonObject(MemberSerialization.Fields)]` but it creates an ugly `k__BackingField` string in the `JSON` string... I wish there was a simple solution like that, without this extra string... – Tar Jan 12 '15 at 09:56

3 Answers3

6

The answer provided by Remi helped me. I took his TupleConverter and made it generic for a 2-tuple. The concept is the same for any N-tuple.

I leave it here in case it helps someone.

public class TupleConverter<U, V> : Newtonsoft.Json.JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(Tuple<U, V>) == objectType;
    }

    public override object ReadJson(
        Newtonsoft.Json.JsonReader reader,
        Type objectType,
        object existingValue,
        Newtonsoft.Json.JsonSerializer serializer)
    {
        if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
            return null;

        var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);

        var target = new Tuple<U, V>(
            jObject["m_Item1"].ToObject<U>(), jObject["m_Item2"].ToObject<V>());

        return target;
    }

    public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

Note: My Tuple was JSON serialized with m_Item1 and m_Item2, so I had to change jObject["ItemX"] to jObject["m_ItemX"]

Usage example with a List<Tuple<int, User>>:

string result = "String to deserialize";
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new TupleConverter<int, User>());
List<Tuple<int, User>> users = JsonConvert.DeserializeObject<List<Tuple<int, User>>>(result, settings);
Sébastien Sevrin
  • 5,267
  • 2
  • 22
  • 39
4

The solution - or mine, anyhow - is to define a custom converter for the Tuple.

This example provides a concrete solution for a specific Tuple, but you could genericize it to make the TupleConverter class to handle any combination of value types. Could also make it abstract and have derived types implement instantiation methods for each item, to handle tuples with reference types.

    public class TupleConverter : Newtonsoft.Json.JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(Tuple<string, bool>) == objectType;
        }

        public override object ReadJson(
            Newtonsoft.Json.JsonReader reader,
            Type objectType,
            object existingValue,
            Newtonsoft.Json.JsonSerializer serializer)
        {
            if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
                return null;

            var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);

            var target = new Tuple<string, bool>(
                (string)jObject["Item1"], (bool)jObject["Item2"]);

            return target;
        }

        public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
        {
            serializer.Serialize(writer, value);
        }
    }

    public class TupleHolder
    {
        [Newtonsoft.Json.JsonConverter(typeof(TupleConverter))]
        public Tuple<string, bool> Tup { get; set; }
        public TupleHolder() { Tup = new Tuple<string, bool>("ZZZ", false); }
        public TupleHolder(string s, bool b) { Tup = new Tuple<string, bool>(s, b); }
    }

    [Test]
    public void Test()
    {
        var orig = new TupleHolder("what????", true);
        var json = Newtonsoft.Json.JsonConvert.SerializeObject(orig);

        Assert.AreEqual("{\"Tup\":{\"Item1\":\"what????\",\"Item2\":true}}", json);

        var dupl = Newtonsoft.Json.JsonConvert.DeserializeObject<TupleHolder>(json);

        // These succeed, now
        Assert.AreEqual(orig.Tup.Item1, dupl.Tup.Item1);
        Assert.AreEqual(orig.Tup.Item2, dupl.Tup.Item2);
    }
Remi Despres-Smyth
  • 4,173
  • 3
  • 36
  • 46
0

I ended up with something more generic, hope it helps

public class TupleConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        var match = Regex.Match(objectType.Name, "Tuple`([0-9])", RegexOptions.IgnoreCase);
        return match.Success;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        try
        {
            var tupleTypes = objectType.GetProperties().ToList().Select(p => p.PropertyType).ToArray();

            var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);

            var valueItems = new List<object>();

            for (var i = 1; i <= tupleTypes.Length; i++)
                valueItems.Add(jObject[$"m_Item{i}"].ToObject(tupleTypes[i - 1]));

            var convertedObject = objectType.GetConstructor(tupleTypes)?.Invoke(valueItems.ToArray());

            return convertedObject;
        }
        catch (Exception ex)
        {
            throw new Exception("Something went wrong in this implementation", ex);
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}