Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSON serialization of generics

I have a class that looked like this:

public class MyData : IList<Tuple<double,double>>

The idea being that you have a list of pairs of values. Simple enough. But I wanted this to be serialized such that it looks like an array of an array of doubles (i.e. double[][]) rather than a list of tuples. It should look like this when serialized:

[[1,1],[2,2],[3,3]]

So I created a simple JsonConverter that will do it. It has a very simple WriteJson method that looks like this:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    var obj = value as MyData;
    double[][] dataArray = (from dp in obj._data
                            select new[] { dp.Item1, dp.Item2 }).ToArray();
    var ser = new JsonSerializer();
    ser.Serialize(writer, dataArray);
}

And I decorate MyData with the appropriate attribute:

    [JsonConverter(typeof(MyDataJsonConverter))]
    public class MyData: IList<Tuple<double,double>>

And everything works fine. Now however, I want to extend this and make MyData generic:

    public class MyData<S,T>: IList<Tuple<S,T>>

And this is were I run into problems. Initially, I tried making MyDataJsonConverter also generic:

   [JsonConverter(typeof(MyDataJsonConverter<S,T>))]
   public class MyData<S,T>: IList<Tuple<S,T>>

But that isn't allowed because you can't use generics with attributes.

So I have to keep MyDataJsonConverter non-generic, but I need to figure out how to flatten my collection of tuples into an array (presumably of object[][] now) such that when I serialize my data would look like:

[[1,2],[2,3]]

or:

[["foo":1],["bar":2]]

or even:

[["foo":"bar"],["what":"ever"]]

Any ideas on how to handle it? In WriteJson, I can no longer cast value to MyData because I won't know the type parameters, so everything just pretty much falls apart at that point.

I could just create separate classes for each type combination (so one for Tuple<double,double> and one for Tuple<string,double> and so on...), but I was wondering if there was a better way before I go brute force on it.

like image 887
Matt Burland Avatar asked Oct 01 '22 19:10

Matt Burland


2 Answers

Okay, so here's basically what I ended up doing (in case anybody else needs something similar). My collection looks something like this:

    [JsonConverter(typeof(MyDataJsonConverter))]
    public class MyData<S,T> : IList<Tuple<S,T>>
    {
        private List<Tuple<S, T>> _data;
        // all the implementation of IList...
    }

And my custom converter (now no longer nested inside MyData) looks like this:

internal class MyDataJsonConverter : JsonConverter
{

    public override bool CanConvert(Type objectType)
    {
        return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(MyData<,>);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Note: this is strictly for serializing, deserializing is a whole other
        // ball of wax that I don't currently need!
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var obj = value as IEnumerable<dynamic>;
        object[][] dataArray = (from dp in obj
                                select new object[] { dp.Item1, dp.Item2 }).ToArray();
        var ser = new JsonSerializer();
        ser.Serialize(writer, dataArray);
    }

    public override bool CanRead
    {
        get
        {
            return false;
        }
    }
}
like image 122
Matt Burland Avatar answered Oct 05 '22 12:10

Matt Burland


The first thing is to use in obj, not to couple yourself to the private members of the class, but to the interface it exposes. Then the only problem you have is to serialize Tuple<S, T> held through an object reference with unknown S and T. You can do it as follows, for example:

public static Tuple<object, object> UnpackUnknownTuple(object tuple)
{
    var item1Property = tuple.GetType().GetProperty("Item1");
    var item2Property = tuple.GetType().GetProperty("Item2");

    return Tuple.Create(
        item1Property.GetValue(tuple, null), 
        item2Property.GetValue(tuple, null));
}

public override void WriteJson(
    JsonWriter writer,
    object value,
    JsonSerializer serializer)
{
    var obj = value as IEnumerable;

    var tuples = obj.Cast<object>().Select(UnpackUnknownTuple);

    object[][] dataArray = (from dp in tuples 
                            select new[] { dp.Item1, dp.Item2 }).ToArray();

    var ser = new JsonSerializer();
    ser.Serialize(writer, dataArray);
}

Another possibility is to convert to string instead of object but I guess it can interfere with some serialization settings. item1Property and item2Property can possibly be cached using first element of the list if there should be any performance problems.

like image 22
BartoszKP Avatar answered Oct 05 '22 10:10

BartoszKP