Home > database >  How do we retain polymorphism after serializing an object to Json
How do we retain polymorphism after serializing an object to Json

Time:05-13

We have a complex issue in our system regarding retaining polymorphism after we have saved a config file to the database.

We have simplified the problem for the sake of this question. See code below.

Here is the code:

class Wheel
{
    public virtual string GetWidth()
    {
        return "Normal";
    }
}

class SmallWheel : Wheel
{
    public override string GetWidth()
    {
        return "Small";
    }
}

class BigWheel : Wheel
{
    public override string GetWidth()
    {
        return "Big";
    }
}

public static async Task Main(string[] args)
{
    //Build list of wheels
    var wheels = new List<Wheel>()
    {
        new Wheel(),
        new SmallWheel(),
        new BigWheel(),
    };

    //We print wheels to check it works
    foreach (var x in wheels)
    {
        Console.WriteLine(x.GetWidth());
    }

    //Save the list to the db as a string
    var file = JsonConvert.SerializeObject(wheels);

    //We just use file instead of db for simplictiydf

    //Later we read the config file from the DB
    var wheelsFromDb = JsonConvert.DeserializeObject<List<Wheel>>(file);

    //We now want to print out our wheels.
    Console.WriteLine("Printing WHeels from Db");
    foreach (var x in wheelsFromDb)
    {
        Console.WriteLine(x.GetWidth());
    }
}

Results when I run it:

Normal
Small
Big

Printing WHeels from Db
Normal
Normal
Normal

Now as you can see we are losing what type the wheel is after de-serialisation.

How can we go about solving this issue?

In essence, we need to store a config file with multiple children classes each with overrides functions. I realise that when Json deserializes the raw string it has no context of what type the class was before. Is there a way we can store that context or use a different library or database?

CodePudding user response:

I am using this code for a list of derived classes. You can use a base class instead of an interface as well

IAnimal[] animals = new IAnimal[] {
                    new Cat{CatName="Tom"},
                    new Dog{DogName="Scoopy"},
                    new Rabbit{RabitName="Honey"}
    };

    var jsonSerializerSettings = new JsonSerializerSettings()
    {
        TypeNameHandling = TypeNameHandling.All
    };
    var json = JsonConvert.SerializeObject(animals, jsonSerializerSettings);
    

    List<IAnimal> animalsBack = ((JArray)JsonConvert.DeserializeObject(json)).Select(o => (IAnimal)JsonConvert.DeserializeObject(o.ToString(), Type.GetType((string)o["$type"]))).ToList();

classes

public interface IAnimal
{
}

public class Animal : IAnimal
{
}
    
public class Cat : IAnimal { public string CatName { get; set; } }
public class Dog : IAnimal { public string DogName { get; set; } }
public class Rabbit : IAnimal { public string RabitName { get; set; } }

CodePudding user response:

JsonConvert creates property $type under the hood when TypeNameHandling.All is used.

So we can give a clue to compiler about what type actually is by creating Size property in classes:

public class Wheel
{
    public virtual string Size { get;  set; } = WheelSize.NORMAL;

    public virtual string GetWidth() => Size;
    
}

public class SmallWheel : Wheel
{
    public override string Size { get; set; } = WheelSize.SMALL;
}

public class BigWheel : Wheel
{
    override public string Size { get; set; } = WheelSize.BIG;
}

This is a class which contains size of wheels:

public class WheelSize
{
    public static string SMALL = "small";
    public static string NORMAL = "normal";
    public static string BIG = "big";
}

So far, so good. But how we can deserialize json based on size wheel? We can write custom deserializer:

internal class WheelJsonConverter : JsonConverter
{
    private readonly Type[] _types;

    public WheelJsonConverter(params Type[] types)
    {
        _types = types;
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
        object existingValue, JsonSerializer serializer)
    {
        List<Wheel> wheels = new List<Wheel>();
        WheelToSize wheelToSize = new WheelToSize();

        JArray jArray = JArray.Load(reader);
        foreach (JToken token in jArray)
        {
            string wheelSize = token["size"].ToString();
            Wheel wheel =  wheelToSize.WheelBySize[wheelSize];
            wheels.Add(wheel);
        }

        return wheels;
    }


    public override bool CanConvert(Type objectType)
    {   
        if (objectType.IsGenericType 
            && IsGenericTypeArgumentTheSame(objectType))            
            return true;
        
        return false;
    }

    private bool IsGenericTypeArgumentTheSame(Type objectType) => 
        objectType.GenericTypeArguments[0] == _types[0];

    public override void WriteJson(JsonWriter writer, object? value, 
        JsonSerializer serializer)
    {
        JToken t = JToken.FromObject(value);

        if (t.Type != JTokenType.Object)
        {
            t.WriteTo(writer);
        }
        else
        {
            JObject o = (JObject)t;
            o.WriteTo(writer);
        }
    }
}

This is a class to create json in lower case:

public class LowercaseContractResolver : DefaultContractResolver
{
    protected override string ResolvePropertyName(string propertyName)
    {
        return propertyName.ToLower();
    }
}   

This is a factory which creates an object based on size:

public class WheelToSize
{
    public Dictionary<string, Wheel> WheelBySize { get; private set; } = new()
    {
        { WheelSize.NORMAL, new Wheel() },
        { WheelSize.BIG, new BigWheel() },
        { WheelSize.SMALL, new SmallWheel() }
    };
}

And then you can run code like this:

List<Wheel> wheels = new List<Wheel>()
{
    new Wheel(),
    new SmallWheel(),
    new BigWheel(),
};

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.ContractResolver = new LowercaseContractResolver();
var file = JsonConvert.SerializeObject(wheels, Formatting.Indented, settings);

var wheelsFromDb = JsonConvert.DeserializeObject<List<Wheel>>(file, 
    new WheelJsonConverter(typeof(Wheel)));

//We now want to print out our wheels.
Console.WriteLine("Printing WHeels from Db");
foreach (var x in wheelsFromDb)
    Console.WriteLine(x.GetWidth());
  • Related