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;
}
}
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.
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;
}
}
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.
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?
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