Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Json.NET custom serialization/deserialization of a third party type

Tags:

c#

json.net

I want to converts Vectors of the OpenTK library to and from JSON. The way I thought it worked is just making a custom JsonConverter, so I did this:

class VectorConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Vector4);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = JToken.Load(reader);
        if (obj.Type == JTokenType.Array)
        {
            var arr = (JArray)obj;
            if (arr.Count == 4 && arr.All(token => token.Type == JTokenType.Float))
            {
                return new Vector4(arr[0].Value<float>(), arr[1].Value<float>(), arr[2].Value<float>(), arr[3].Value<float>());
            }
        }
        return null;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var vector = (Vector4)value;
        writer.WriteStartArray();
        writer.WriteValue(vector.X);
        writer.WriteValue(vector.Y);
        writer.WriteValue(vector.Z);
        writer.WriteValue(vector.W);
        writer.WriteEndArray();
    }
}

Now, the Write part is pretty straight forward to me (I think?). When the serializer goes through objects, if it encounters one that the CanConvert method responds to with true, it lets my custom serializer to convert it to JSON. And that works.

What I don't really get is the other way. Since there's no way to know what type something is when it's just written in literals in JSON, I thought I have to analyse the object myself and determine if it's in fact the Vector object. The code I've written works, but I don't know what to do if the check fails. How do I tell the deserializer that this isn't one of the objects that I know how to translate and that it should do it's default thing on it?

Am I missing something in how the whole thing works?

like image 674
Luka Horvat Avatar asked Oct 03 '22 03:10

Luka Horvat


1 Answers

During deserialization, Json.Net looks at the classes that you are deserializing into in order to determine what types to create, and by extension, whether to call your converter. So, if you deserialize into a class that has a Vector4 property, your converter will be called. If you deserialize into something nebulous like dynamic or object or JObject, then Json.Net will not know to call your converter, and therefore the deserialized object hierarchy will not contain any Vector4 instances.

Let's take a simple example to make this concept more clear. Say we have this JSON:

{
    "PropA": [ 1.0, 2.0, 3.0, 4.0 ],
    "PropB": [ 5.0, 6.0, 7.0, 8.0 ]
}

Clearly, both 'PropA' and 'PropB' in the above JSON could represent a Vector4 (or at least what I infer to be a Vector4 from your converter code-- I am not actually familiar with the OpenTK library). But, as you noticed, there is no type information in the JSON that says that either property should be a Vector4.

Let's try to deserialize the JSON into the following class using your converter. Here, PropA must contain a Vector4 or null since it is strongly typed, while PropB could be anything.

public class Tester
{
    public Vector4 PropA { get; set; }
    public object PropB { get; set; }
}

Here is the test code:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""PropA"": [ 1.0, 2.0, 3.0, 4.0 ],
            ""PropB"": [ 5.0, 6.0, 7.0, 8.0 ]
        }";

        try
        {
            Tester t = JsonConvert.DeserializeObject<Tester>(json),
                                              new VectorConverter());

            DumpObject("PropA", t.PropA);
            DumpObject("PropB", t.PropB);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.GetType().Name + ": " + ex.Message);
        }
    }

    static void DumpObject(string prop, object obj)
    {
        if (obj == null)
        {
            Console.WriteLine(prop + " is null");
        }
        else
        {
            Console.WriteLine(prop + " is a " + obj.GetType().Name);
            if (obj is Vector4)
            {
                Vector4 vector = (Vector4)obj;
                Console.WriteLine("   X = " + vector.X);
                Console.WriteLine("   Y = " + vector.Y);
                Console.WriteLine("   Z = " + vector.Z);
                Console.WriteLine("   W = " + vector.W);
            }
            else if (obj is JToken)
            {
                foreach (JToken child in ((JToken)obj).Children())
                {
                    Console.WriteLine("   (" + child.Type + ") " 
                                             + child.ToString());
                }
            }
        }
    }
}

// Since I don't have the OpenTK library, I'll use the following class
// to stand in for `Vector4`.  It should look the same to your converter.

public class Vector4
{
    public Vector4(float x, float y, float z, float w)
    {
        X = x;
        Y = y;
        Z = z;
        W = w;
    }

    public float W { get; set; }
    public float X { get; set; }
    public float Y { get; set; }
    public float Z { get; set; }
}

When I run the test code, here is the output I get:

PropA is a Vector4
   X = 1
   Y = 2
   Z = 3
   W = 4
PropB is a JArray
   (Float) 5
   (Float) 6
   (Float) 7
   (Float) 8

So you can see, for PropA, Json.Net used the converter to create the Vector4 instance (otherwise we would have gotten a JsonSerializationException), while for PropB, it did not (otherwise, we would have seen PropB is a Vector4 in the output).

As for the second part of your question, what to do if your converter is given JSON that is not what it is expecting. You have two choices-- return null, like you are doing, or throw an exception (such as a JsonSerializationException). If your converter is being called, you know that Json.Net is trying to populate a Vector4 object. If it were not, then your converter would not have been called. So, if you can't populate it because the JSON is wrong, you have to decide whether it is acceptable that the Vector4 be null, or is it better to error out. It is a design decision that depends on what you are trying to do in your project.

Have I explained that clearly?

like image 179
Brian Rogers Avatar answered Oct 13 '22 10:10

Brian Rogers