Newtonsoft.Json JsonConvert.DeserializeObject (Polymorphism)

Hey guys, I’m using JsonConvert.DeserializeObject to deserialize animals from the Animal list, but unfortunately they get deserialized as Animals instead of their derived classes (Cat and Dog). As you can see, the needed information is being serialized (“$type”). Does anyone know how to solve this?

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using UnityEngine;

public class SerializationExample : MonoBehaviour
{
    void Start()
    {
        var dog1 = ScriptableObject.CreateInstance<Dog>();
        dog1.age = 6;
        dog1.breed = "Bulldog";

        var cat1 = ScriptableObject.CreateInstance<Cat>();
        cat1.age = 7;
        cat1.lives = 5;

        var zoo1 = ScriptableObject.CreateInstance<Zoo>();
        zoo1.animals = new Animal[] { dog1, cat1 };

        JsonSerializerSettings settings = new() { Formatting = Formatting.Indented, TypeNameHandling = TypeNameHandling.Auto };
        settings.Converters.Add(new ScriptableObjectCreationConverter<Cat>());
        settings.Converters.Add(new ScriptableObjectCreationConverter<Dog>());
        settings.Converters.Add(new ScriptableObjectCreationConverter<Animal>());
        settings.Converters.Add(new ScriptableObjectCreationConverter<Zoo>());

        string json = JsonConvert.SerializeObject(zoo1, settings);
        Debug.Log(json);

        /*
        {
          "animals": [
            {
              "$type": "Dog, Project",
              "breed": "Bulldog",
              "age": 6,
              "name": "",
              "hideFlags": 0
            },
            {
              "$type": "Cat, Project",
              "lives": 5,
              "age": 7,
              "name": "",
              "hideFlags": 0
            }
          ],
          "name": "",
          "hideFlags": 0
        }
        */

        zoo1 = JsonConvert.DeserializeObject<Zoo>(json, settings);
        Debug.Log(zoo1.animals[0] is Dog); // returning false instead of true!
        Debug.Log(zoo1.animals[1] is Cat); // returning false instead of true!
    }
}

public class Zoo : ScriptableObject
{
    public Animal[] animals;
}

public class Animal : ScriptableObject
{
    public int age;
}

public class Dog : Animal
{
    public string breed;
}

public class Cat : Animal
{
    public int lives;
}

public class ScriptableObjectCreationConverter<T> : CustomCreationConverter<T> where T : ScriptableObject
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T) == objectType;
    }

    public override T Create(Type objectType)
    {
        if (typeof(T) == objectType) return (T)ScriptableObject.CreateInstance<T>();
        return default;
    }
}

Have you debugged to find out if your CanConvert() method (of your custom converters) is even being queried and with what type?

If so, is the objectType wrong? etc…

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

1 Like

json doesn’t inherently store type information so newtonsoft infers the type from T passed to DeserializeObject.

Newtonsoft does have a feature to support what you want, it’s called ‘TypeNameHandling’:

Note that this feature can be considered insecure though because it means that someone can feed json into your project that contains types that you don’t mean to deserialize causing your application to operate code not intended to operate. Usually how you deal with this is to explicitly say what types are supported when deserializing… but mind you this method isn’t perfect either.

What you’ll want to do is set the SerializationBinder of the jsonserializersettings to control what types can and can not be bound to:

Looking at your code it looks like you’re setting most everything up, it’s just not binding to the type. I’d create a simple binder implementation, attach it, and debug out the binding process.

3 Likes

Thanks for the help guys. I gave SerializationBinder using the TypeNameHandling a try but ended up serializing the Type myself.

It is indeed insecure, but is there any reason for me to worry about it? It’s a single player game where I don’t care if the player “hacks” the save file. If you know a reason, please tell me about it!

Here is the final working code, in case anyone is interested:

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using UnityEngine;

public class SerializationExample : MonoBehaviour
{
    void Start()
    {
        var dog1 = ScriptableObject.CreateInstance<Dog>();
        dog1.age = 6;
        dog1.breed = "Bulldog";

        var cat1 = ScriptableObject.CreateInstance<Cat>();
        cat1.age = 7;
        cat1.lives = 5;

        var zoo1 = ScriptableObject.CreateInstance<Zoo>();
        zoo1.animals = new Animal[] { dog1, cat1 };

        JsonSerializerSettings settings = new() { Formatting = Formatting.Indented };
        settings.Converters.Add(new ScriptableObjectConverter<Cat>());
        settings.Converters.Add(new ScriptableObjectConverter<Dog>());
        settings.Converters.Add(new ScriptableObjectConverter<Animal>());
        settings.Converters.Add(new ScriptableObjectConverter<Zoo>());

        string json = JsonConvert.SerializeObject(zoo1, settings);
        Debug.Log(json);

        /*
        {
          "$type": "Zoo",
          "animals": [
            {
              "$type": "Dog",
              "breed": "Bulldog",
              "age": 6
            },
            {
              "$type": "Cat",
              "lives": 5,
              "age": 7
            }
          ]
        }
        */

        zoo1 = JsonConvert.DeserializeObject<Zoo>(json, settings);
        Debug.Log(zoo1.animals[0] is Dog); // finally returning true
        Debug.Log(zoo1.animals[1] is Cat); // finally returning true
    }
}

public class Zoo : ScriptableObject
{
    public Animal[] animals;
}

public class Animal : ScriptableObject
{
    public int age;
}

public class Dog : Animal
{
    public string breed;
}

public class Cat : Animal
{
    public int lives;
}

public class ScriptableObjectConverter<T> : JsonConverter where T : ScriptableObject
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T) == objectType;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var jsonObject = new JObject
        {
            { "$type", JToken.FromObject(value.GetType().FullName, serializer) } // alternatively AssemblyQualifiedName
        };

        // Serialize properties
        foreach (var prop in value.GetType().GetProperties())
        {
            if (prop.Name == "name" || prop.Name == "hideFlags") continue;
            if (prop.CanRead) jsonObject.Add(prop.Name, JToken.FromObject(prop.GetValue(value), serializer));
        }

        // Serialize fields
        foreach (var field in value.GetType().GetFields())
        {
            jsonObject.Add(field.Name, JToken.FromObject(field.GetValue(value), serializer));
        }

        jsonObject.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jsonObject = JObject.Load(reader);
        var typeName = jsonObject["$type"]?.ToString();
        if (typeName == null) throw new JsonSerializationException("Missing $type field.");

        var type = Type.GetType(typeName);
        if (type == null) throw new JsonSerializationException($"Unknown type: {typeName}");

        var obj = ScriptableObject.CreateInstance(type);
        serializer.Populate(jsonObject.CreateReader(), obj);
        return obj;
    }
}

Cheers!

At this point the main concern is that the save file becomes a vector for an attack for your users. Basically if your game becomes popular enough that the exploit is known, someone could put a tainted save file on the internet and convince someone to download it and load up the save. The security flaw impacts the user, not you directly.

Looking at your code though… your logic does block it in some manners. For instance you assume all types fed in are ScriptableObjects so the security issue is less an issue since well… your game likely doesn’t have any ScriptableObjects that can really do much of anything.

Just note these security concerns since you do use ‘Type.GetType’:

(effectively don’t let an assemblyresolver load up any unknown assemblies, currently you should be fine)

If you wanted though just to be extra extra safe… and it could maybe even give you a boost in performance since Type.GetType does a bit of lifting, is just have a dictionary of accepted types based on the name stored in the json.

2 Likes

Thank you for the response!

I had thought about that scenario and kind of chose to ignore it… but now that you put it that way, I think it’s worth doing something about it - for the users’ sake.

I hadn’t though about that!

If you say I should be fine, that’s great. I’m a bit out of my depth here haha.

I could do that, but if I renamed a class, I’d have to change that dictionary too. And every time I added a new class, I’d have to remember to go and change that same dictionary. What about adding a simple tag Attribute to the ones that should serializable using the converter and then in the ReadJson method just check if the type has that attribute?

This is the usefulness of the ‘nameof’ keyword:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/nameof

If a property, field, member, anything changes name and any stringified version you used uses this… your compiler with throw errors cause it doesn’t know the thing nameof is pointing at.

You can also do:
typeof(Zoo).FullName
typeof(Zoo).Name

depending on what format of the name you intended

2 Likes

That’s what I’ll do. By using a dictionary, I can completely bypass the need for ‘Type.GetType’. Thanks!