4

I am responsible for maintaining a game system where users persist JSON serialized POCOs in a local cache to preserve state of, e.g., a Character.

The newest version of the code has changed the data model of these serialized objects. In particular, a new interface was created. This is creating issues when deserializing old copies of characters into the new code. I am attempting to resolve these with custom converters, but I'm running into trouble.

old, serialized version:

public class Character{
  public Skill Parent {get;set;}
  public Dictionary<string,Skill} Skills {get;set;}
}

public class Skill {
//normal stuff here.
}

new version:

public class Character{
  [JsonProperty, JsonConverter(typeof(ConcreteTypeConverter<Dictionary<string,Skill>>))]
  public Dictionary<string,ISkill} Skills {get;set;}
}

public class Skill:ISkill {
//normal stuff here.
}

public interface ISkill{
//stuff that all skill-like things have here
}

I have further defined a custom converter class (having read this and this,

but i'm still running into trouble deserializing collections.

public class Extensions 
{

//a lot of serializer extensions here including the Deserialize method
 private static readonly CustomSerializationBinder Binder = new CustomSerializationBinder();
        private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
        {
            NullValueHandling = NullValueHandling.Ignore,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            TypeNameHandling = TypeNameHandling.Objects,
            TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple,
            Binder = Binder,
        };
}


 public class CustomSerializationBinder : DefaultSerializationBinder
    {


        public override Type BindToType(string assemblyName, string typeName)
        {

             return base.BindToType(assemblyName, typeName);


        }
    }

    public class ConcreteTypeConverter<T> : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            serializer.Serialize(writer,value); // serialization isnt't the problem.
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (typeof (T)==typeof(Dictionary<string,Skill>))
            {
                var retVal = new object();
                if (reader.TokenType == JsonToken.StartObject)
                {
                    T instance = (T)serializer.Deserialize(reader, typeof(T));  //crashes here
                    retVal = new List<T>() { instance };
                    return retVal;
                }
            }

            return serializer.Deserialize<T>(reader);
        }

        public override bool CanConvert(Type objectType)
        {
            return true; // kind of a hack 
        }
    }

So i have an old Dictionary<string,Skill> and I can't cast that to Dictionary<string,ISkill> in any code-path that I can see. How should I resolve this?

Community
  • 1
  • 1
gvoysey
  • 516
  • 1
  • 4
  • 15
  • Take a look at https://stackoverflow.com/questions/33321698/jsonconverter-with-interface. This gives a generic converter that will determine which concrete `Skill` class to deserialize for an `ISkill` interface by using `$type` if present, or if not, choosing the best property match. – dbc Feb 18 '16 at 20:12
  • I'll have a look. I'm already using `$type` to solve some problems (at the expense of bloat), so maybe this is a good way forward. The problem i'm worried about is that the stale JSON is going to have a `$type` that's `Skill`, not `ISkill` -- and also, how to handle a `$type` that's `Dictionary`. – gvoysey Feb 18 '16 at 20:41
  • I'm not 100% sure I understand your problem. Does your legacy JSON (from before the `Skill` to `ISkill` refactoring) contain `$type` information for the dictionary itself? – dbc Feb 18 '16 at 21:30
  • Yes. legacy JSON would look like `$type: "System.Collections.Generic.Dictionary'2[[System.String, mscorlib],[DataModel.Skill, PROJECT_NAME]]"` in that case. – gvoysey Feb 18 '16 at 21:33

1 Answers1

0

Since your legacy JSON already contains type information for all objects including dictionary objects, what you need to do is to strip the type information for dictionaries and allow the deserialized dictionary type to be controlled by the code, not the JSON.

The following converter should do the job:

public class IgnoreDictionaryTypeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType.GetDictionaryKeyValueTypes().Count() == 1;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        existingValue = existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
        var obj = JObject.Load(reader);
        obj.Remove("$type");
        using (var subReader = obj.CreateReader())
        {
            serializer.Populate(subReader, existingValue);
        }
        return existingValue;
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public static class TypeExtensions
{
    /// <summary>
    /// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type.IsInterface)
            return new[] { type }.Concat(type.GetInterfaces());
        else
            return type.GetInterfaces();
    }

    public static IEnumerable<Type[]> GetDictionaryKeyValueTypes(this Type type)
    {
        foreach (Type intType in type.GetInterfacesAndSelf())
        {
            if (intType.IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
            {
                yield return intType.GetGenericArguments();
            }
        }
    }
}

Then you could add it in settings, or apply it to the dictionary property in question:

public class Character
{
    [JsonConverter(typeof(IgnoreDictionaryTypeConverter))]
    public IDictionary<string, ISkill> Skills { get; set; }
}

For the future, you might also want disable emitting of type information for dictionaries, since dictionaries are collections, and collection types are better specified by the code, not the JSON:

public class Character
{
    [JsonConverter(typeof(IgnoreDictionaryTypeConverter))]
    [JsonProperty(TypeNameHandling = TypeNameHandling.None)]
    public IDictionary<string, ISkill> Skills { get; set; }
}
dbc
  • 104,963
  • 20
  • 228
  • 340