Search code examples
c#unity-game-enginejson.netdeserialization

Why is my custom Json.net converter not working?


Since GameObjects cannot be serialized I wrote a class that holds the name of the GameObject so that it can find it by the name. However I am getting this error during deserialization and I can't figure out why.

JsonSerializationException: Type specified in JSON 'GameObjectReferencer, SaveSystem, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' is not compatible with 'UnityEngine.GameObject, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. Path 'wrappedSaveDatas.$values[4].savedObject.other.$type', line 338, position 55.

This is my Custom converter:

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

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{        throw new Exception("GameObjectFromReferenceConverter should only be used when loading");
}


public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    GameObjectReferencer goRef = (GameObjectReferencer)reader.Value;

    
    return ((GameObjectReferencer)goRef).GetGameObject();
}
}

This is my Referencer class:

 public class GameObjectReferencer
{
public string targetGameObjectName;

public GameObjectReferencer(string gameObjectName)
{
    this.targetGameObjectName = gameObjectName;  
}

public GameObject GetGameObject()
{
    GameObject go = GameObject.Find(targetGameObjectName);
    if (go == null)
        throw new System.Exception("Can't find object with the name " + targetGameObjectName);
    return go;
}

public override string ToString()
{
    return "GameObjectReference of \"" + targetGameObjectName+ "\"";
}

public static GameObjectReferencer GenerateReference(GameObject gameObject)
{
    return new GameObjectReferencer(gameObject.name);
}

}

Solution

  • Your GameObjectReferencer is an example of a DTO (data transfer object)

    a data transfer object (DTO) is an object that carries data between processes.

    Since serialization and deserialization need to have a consistent format, that generally means that your DTO must get used for both. Thus your converter should look something like the following:

    public class GameObjectFromReferenceConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType) => 
            typeof(GameObject).IsAssignableFrom(objectType); 
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
            serializer.Serialize(writer, GameObjectReferencer.GenerateReference((GameObject)value));
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) =>
            serializer.Deserialize<GameObjectReferencer>(reader)?.GetGameObject();
    }
    
    // Serialization DTO class for GameObject and derived types.
    class GameObjectReferencer
    {
        public string targetGameObjectName { get; set; }
    
        public GameObjectReferencer(string targetGameObjectName) => this.targetGameObjectName = targetGameObjectName;
    
        public GameObject GetGameObject() =>
            GameObject.Find(targetGameObjectName) ?? throw new System.Exception("Can't find object with the name " + targetGameObjectName);
    
        public static GameObjectReferencer GenerateReference(GameObject gameObject) => new GameObjectReferencer(gameObject.name);
    
        public override string ToString() => "GameObjectReference of \"" + targetGameObjectName+ "\"";
    }
    

    And you must also use the converter in settings for both serialization and deserialization, e.g. like so:

    var settings = new JsonSerializerSettings
    {
        Converters = { new GameObjectFromReferenceConverter() },
        // Other settings as required, e.g.:
        TypeNameHandling = TypeNameHandling.Auto,
    };
    

    Notes:

    • When serializing with a converter, the converter must take care of all aspects of serialization and deserialization, including serialization and deserialization of $type metadata. Thus converters do not necessarily play well with Json.NET's TypeHandHandling setting.

    • When deserializing, the objectType passed to JsonConverter.CanConvert(objectType) will be the declared type of the object to be deserialized. Since the declared type of any GameObject reference will typically be GameObject or some subtype, in CanConvert you need to check that the type is assignable from GameObject not GameObjectReferencer.

      When serializing the objectType will be the actual, concrete type being serialized, which will, again, be some subtype of GameObject.

    • Inside ReadJson() the value of JsonReader.Value will be the value of the current json token. When the current token is JsonToken.StartObject (i.e. positioned on the { token), JsonReader.Value will be null.

    • For Json.NET to successfully deserialize a type using a parameterized constructor, the constructor argument names must match the property names (here targetGameObjectName).

    • When using TypeNameHandling, do take note of this caution from the Newtonsoft docs:

      TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.

      For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json.

    • For a similar converter using GUIDs instead of names, see How can I custom serialize a GameObject reference by its GUID?.

    Mockup fiddle here.