Bastiaan
Bastiaan

Reputation: 736

Cast object, de-serialized as an interface, back to its original type

namespace Animals
{
    interface IAnimal
    {
        string MakeNoise();
    }

    class Dog : IAnimal
    {
        public string MakeNoise()
        {
            return "Woof";
        }
    }

    class Cat : IAnimal
    {
        public string MakeNoise()
        {
            return "Miauw";
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Dog dog1 = new Dog();
            dog1.Name = "Fikkie";

            var jsonAnimal = JsonConvert.SerializeObject(dog1);
            IAnimal animal = JsonConvert.DeserializeObject<IAnimal>(jsonAnimal) as IAnimal;
            animal.MakeNoise();
        }
    }
}

I am serializing and storing several Dog and Cat classes of which all implement the IAnimal interface, which contain all the properties and methods I need. When de-serializing I need to get them to have the IAnmial interface again.

The DeserializeObject now throws an error: 'Could not create an instance of type Animals.IAnimal. Type is an interface or abstract class and cannot be instantiated.

I cannot change the Newtonsoft execution as this is happening in a class that's not accessible, so unfortunately the TypeNameHandling = TypeNameHandling.All is not possible. The method does expect a type, so it seems the exact implementation as I have in this example.

Upvotes: 1

Views: 1204

Answers (2)

user2864740
user2864740

Reputation: 61975

The reported error is because interfaces cannot be instantiated and, by default, Json.NET has no rules/knowledge of how to map suitable concrete types from arbitrary interfaces. However, simply using a non-abstract base class that, unlike an interface, can be instantiated is insufficient. If this were done an instance of the base type would be created on deserialization: this is incorrect as the original class type is expected to be instantiated.

For a round-trip process to work through a polymorphic base type, such as IAnimal (or an Animal base class), enough information about the concrete type needs to be saved in the JSON. On deserialization this information is used to create an instance of the original concrete type.

Given the issue/approach identified above, and stated restrictions, this task can be solved using a custom JsonConverter with a JsonConverterAttribute on the IAnimal interface+.

  • The converter is responsible to store metadata and create instances of the original concrete type.

    In the provided implementation this uses Json.NET’s built-in support through TypeNameHandling which “..include[s] type information when serializing JSON and read[s] type information so that the [original] types are created when deserializing JSON.”

  • The attribute allows the converter to be used without explicitly adding it to the serialization or deserialization call-sites.

+This approach will work if either IAnimal has the attribute added, or a serializable wrapper type has the converter attribute added to an IAnimal property. As a potential downside, this approach requires implementation contract guarantees – ie. that Json.NET is used.


This code runs in LINQPad as a Program, when referencing Json.NET version 12 and adding Newtonsoft.Json as an additional namespace.

// using Newtonsoft.Json

public void Main() {
    var dog = new Dog();
    var json = JsonConvert.SerializeObject(dog);
    // json -> {"$type":"UserQuery+Dog, query_mtutnt"}
    var anAnimal = JsonConvert.DeserializeObject<IAnimal>(json);
    Console.WriteLine($"{anAnimal.GetType().Name} says {anAnimal.Noise}!");
    // -> Dog says Woof!
}

[JsonConverterAttribute(typeof(AnimalConverter))]
public interface IAnimal
{
    [JsonIgnore] // don’t save value in JSON
    string Noise { get; }
}

public class Dog : IAnimal
{
    public string Noise => "Woof";
}

public class Cat : IAnimal
{
    public string Noise => "Meow";
}

internal sealed class AnimalConverter : JsonConverter
{
    // Have to prevent the inner serialization from infinite recursion as
    // this code is leveraging the built-in TypeNameHandling support.
    // This approach will need updates if there are nested IAnimal usages.
    [ThreadStatic]
    private static bool TS_Converting;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var mySerializer = JsonSerializer.Create(new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });

        try
        {
            TS_Converting = true;
            mySerializer.Serialize(writer, value);
        }
        finally
        {
            TS_Converting = false;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var mySerializer = JsonSerializer.Create(new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });

        return mySerializer.Deserialize(reader);
    }

    public override bool CanRead
    {
        get { return true; }
    }

    public override bool CanWrite
    {
        get { return !TS_Converting; }
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(IAnimal).IsAssignableFrom(objectType);
    }
}

As seen in the resulting JSON, shown as {"$type":"UserQuery+Dog, query_mtutnt"}, the generalized approach of encoding the full type name can be problematic if the type’s namespace or assembly is changed. (Trivially, the JSON is not transferable between LINQPad runs due to the changing randomized assembly name.)

If this were my project, I might write the converter to to use a JSON structure such as ["dog", {"age": 2}] and use reflection/attributes to map "dog" to Dog (a known subtype of IAnimal). However, a ‘better’ domain encoding falls outside of the original question and such can be explored in a refined follow-up as warranted.

On a final note, when the library cannot be guaranteed to use Json.NET and it “must” be used to serialize/deserialize, it may still be possible to manually map-to/from a Object-Graph representation (eg. dictionaries, arrays, and primitives) that can be round-tripped though the library. This still involves encoding the concrete type on map-to so that it can used to create the correct instance on map-from.

Upvotes: 1

dotnetstep
dotnetstep

Reputation: 17485

I think if you want dynamic way then you have to do following thing in serialization and deserialization process.

var jsonAnimal = JsonConvert.SerializeObject(dog1, new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.All
            });

IAnimal animal = JsonConvert.DeserializeObject(jsonAnimal, new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.All
            }) as IAnimal;

Upvotes: 1

Related Questions