-2

I have a simple use case where I want to deserialize a JSON that is basically an array of items, items are not identical but they all share a base class.

UPDATE: my technical limitations:

  • I receive the JSON from a webhook and can't alter the serialization code or inject any tokens in the source JSON
  • The Type property is the only information to do the correspondence between the derived class that I want to deserialize to
  • need strongly typed instances and not dynamic ones
  • The classes are in an assembly and I can't add any Json Annotations

Here is the code:

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
                    
public class Program
{
    public static void Main()
    {
        var json = @" [{""Type"":0},{""Name"":""Derived"",""Type"":1}]";
        
        var deserializedInstances = JsonConvert.DeserializeObject<List<BaseClass>>(json);
        foreach(var e  in deserializedInstances) {
            if(e is BaseClass baseClass) 
            {
                Console.WriteLine("Base Class , Type = {0}", baseClass.Type);
            }else if(e is DerviedClass derivedClass) 
            {
                Console.WriteLine("Derived Class , Type = {0}, Name = {1}", derivedClass.Type, derivedClass.Name);
            }
        }
        // Output 
        // Base Class , Type = 0
        // Base Class , Type = 0
        
        
    }
    
    public class BaseClass   
    {
        public virtual int Type  =>0;
    }
    public class DerviedClass:  BaseClass   
    {
        public string  Name  {get; set;}
        public override int Type =>1;
    }
}

so this code will produce this output:

        // Base Class , Type = 0
        // Base Class , Type = 0

but in my case, I want to have the instance of the derived class.

        // Base Class , Type = 0
        // Base Class , Type = 1, Name = "Derived"

What is the best way to achieve this in terms of performance?

Benzara Tahar
  • 2,058
  • 1
  • 17
  • 21
  • You are deserializing to the BaseClass... how is newtonsoft supposed to know that data is for derived class? You can probably deserialize to derived class and if name is null, its BaseClass – Jawad Jan 20 '21 at 14:42
  • yep that's the essence of my question – Benzara Tahar Jan 20 '21 at 14:43
  • 3
    check this solution : https://stackoverflow.com/questions/8513042/json-net-serialize-deserialize-derived-types – Mohammed Sajid Jan 20 '21 at 14:44
  • in my case I have the JSON and the Class definitions, I need to be able to deserialize the class properly. – Benzara Tahar Jan 20 '21 at 14:44
  • 1
    You _could_ deserialize to `dynamic` ... and then go on from there. – Fildor Jan 20 '21 at 14:47
  • in my case, I don't have access to the serialization part, I have only the JSON text and the Type property that can be used as a discriminator of the Derived type – Benzara Tahar Jan 20 '21 at 14:52
  • 1
    ^^ https://dotnetfiddle.net/wk68tl – Fildor Jan 20 '21 at 15:10
  • I appreciate putting that into a dotnetfiddle, in my case I have around 20 Events that derives from a base event class, your solution works but it's cumbersome to implement it for all events :/ – Benzara Tahar Jan 20 '21 at 15:13
  • 1
    It's just a PoC. The code shown is far from "production-grade". I rarely, almost never, have to use dynamic, so there surely are optimized algorithms to get to your desired strong type, that I am not aware of. – Fildor Jan 20 '21 at 15:20
  • In my updated answer, I added some code, to have properties automatically filled via reflection, so you basically need just a mapping from `Type` value to your actual type and you're done. – Fildor Jan 20 '21 at 16:43

3 Answers3

2

As Jawad pointed out, you are deserializing to the BaseClass so the objects will also be of type BaseClass and not extend beyond that.

What you want to do is something akin to this answer: Json.net serialize/deserialize derived types?

The short answer is you have to account for settings when deserializing, more specifically Type Name handling. Copied from the answer:

Base object1 = new Base() { Name = "Object1" };
Derived object2 = new Derived() { Something = "Some other thing" };
List<Base> inheritanceList = new List<Base>() { object1, object2 };

JsonSerializerSettings settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
string Serialized = JsonConvert.SerializeObject(inheritanceList, settings);
List<Base> deserializedList = JsonConvert.DeserializeObject<List<Base>>(Serialized, settings);

This will allow the ability to deserialize into derived classes and alike. An example can be found here as well: https://www.newtonsoft.com/json/help/html/SerializeTypeNameHandling.htm

EDIT: In terms of performance I'm not sure there's a much better way of producing the same results that you are looking for.

KillaBytes
  • 447
  • 3
  • 10
  • Yes I saw that but as I said above, in my case, I don't have access to the serialization part, I have only the JSON text and the Type property that can be used as a discriminator of the Derived type – Benzara Tahar Jan 20 '21 at 14:53
  • @BelahceneBenzaraTahar Then you'll need to define that in your questions scope. In order for this to work you will need to access the JSON and inject a `"$type":` and define the scope of that type to your classes (security risk). Otherwise you can also create your own converter to assign which class receives which data such as the examples here: https://stackoverflow.com/questions/6348215/how-to-deserialize-json-into-ienumerablebasetype-with-newtonsoft-json-net – KillaBytes Jan 20 '21 at 14:58
  • 1
    I updated the question, thanks i'll explore the link you sent – Benzara Tahar Jan 20 '21 at 15:00
  • I am trying to make it working using a custom converter but it doesn't seem to work! here is the dotnetfiddle https://dotnetfiddle.net/UcH36B – Benzara Tahar Jan 20 '21 at 15:24
  • @BelahceneBenzaraTahar your converter is working as intended. The issue you are running into is more of distinguishing between the `BaseClass` and `DerivedClass` when looping through the objects. There are several ways to get around this, one noted by Fildor. Another is looping through and noting the object `as` the derived class then checking the object or just deserializing into a list of `objects` (similar to `dynamic`) `dynamic item = serializer.Deserialize(getString); string name = item["name"];` – KillaBytes Jan 20 '21 at 15:53
1

Taking a detour over dynamic to strongly typed:

using System;
using System.Collections.Generic;
using Newtonsoft.Json;

public class Program
{
    public static void Main()
    {
        var a = new BaseClass(){ Type = 0 };
        var b = new DerivedClass(){ Type = 1, Name = "Hello" };
        
        var list = new List<BaseClass>(){a,b};
        var json = JsonConvert.SerializeObject(list);
        Console.WriteLine(json);
        
        var intermediate = JsonConvert.DeserializeObject<List<dynamic>>(json);
        var result = new List<object>();
        foreach( dynamic item in intermediate )
        {
            // Of course, you surely could optimize the conversion:
            if( item.Type == 0 ) result.Add( new BaseClass(){Type = item.Type});
            if( item.Type == 1 ) result.Add( new DerivedClass(){Type= item.Type, Name= item.Name});
        }
        
        Console.WriteLine($"[{string.Join(", ",result)}]");
    }
}

public class BaseClass
{
    public int Type  {get; set;}
}
public class DerivedClass:  BaseClass   
{
    public string  Name  {get; set;}
}

produces output on fiddle:

[{"Type":0},{"Name":"Hello","Type":1}]
[BaseClass, DerviedClass]

Mind, that this is just a proof of concept. Of course, you'd need to fortify and find some decent algorithm to get from dynamic to your desired strong type.


Update

This fiddle shows some possibilities to improve on effort for many Derived Classes: https://dotnetfiddle.net/zECBx5

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class Program
{
    public static void Main()
    {
        var a = new BaseClass(){ Type = 0 };
        var b = new DerivedClass(){ Type = 1, Name = "Hello" };
        
        var list = new List<BaseClass>(){a,b};
        var json = JsonConvert.SerializeObject(list);
        Console.WriteLine(json);
        
        var intermediate = JsonConvert.DeserializeObject<List<dynamic>>(json);
        
        var result = Construct( intermediate );
        
        Console.WriteLine($"[{string.Join(", ",result.Select(x => x?.ToString() ?? "NULL"))}]");
    }
    
    public static List<object> Construct( List<dynamic> items )
    {
        var result = new List<object>();
        Console.WriteLine( $"Processing {items.Count} dynamic items" );
        foreach( dynamic item in items )
        {
            result.Add(Construct( item ));
        }
        return result;
    }
    
    private static Dictionary<int, Func<dynamic, object>> factoryMap = new () {
        {0 , Construct<BaseClass>},
        {1 , Construct<DerivedClass>},
    };
    
    public static object Construct( dynamic item )
    {
        Console.WriteLine($"Item Type = {item.Type}");
        object result = null;
        result = factoryMap[(int)item.Type](item);
        return result;
    }
    
    public static TResult Construct<TResult>( dynamic item ) where TResult: class, new()
    {
        Console.WriteLine($"Constructing a {typeof(TResult).ToString()}");
        TResult result = new();
        foreach( var property in result.GetType().GetProperties() )
        {
            JObject jo = item as JObject;
            var propVal = jo.Property(property.Name).ToObject(property.PropertyType);
            Console.WriteLine($"Setting property {property.Name} to value {propVal}");
            property.SetValue( result, propVal );
        }
        return result;
    }
}

    public class BaseClass
    {
        public int Type  {get; set;}
    }
    public class DerivedClass:  BaseClass   
    {
        public string  Name  {get; set;}
    }
Fildor
  • 14,510
  • 4
  • 35
  • 67
  • Your solution works, although I went with the converter approach, this is the closest solution to what I want and I don't want to accept my own solution so... – Benzara Tahar Jan 20 '21 at 17:09
1

Custom Converters is the solution I was looking for:

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

                    
public class Program
{
    public static void Main()
    {
        var json = @" [{""Type"":0},{""Name"":""Derived"",""Type"":1}]";
        
        var deserializedInstances = JsonConvert.DeserializeObject<List<BaseClass>>(json,
                new JsonSerializerSettings() 
                { 
                    Converters = { new CustomConverter() }
                });
        foreach(var e  in deserializedInstances) {
            if(e is BaseClass baseClass && e.Type == 0) 
            {
                Console.WriteLine("Base Class , Type = {0}", baseClass.Type);
            } 
            if(e is DerviedClass derivedClass) 
            {
                Console.WriteLine("Derived Class , Type = {0}, Name = {1}", derivedClass.Type, derivedClass.Name);
            }
        }   
        // output
        // Base Class , Type = 0
        // Derived Class , Type = 1, Name = Derived
    }
    
    public class BaseClass   
    {
        public  int Type {get; set;}
    }
    public class DerviedClass:  BaseClass   
    {
        public string  Name  {get; set;}
    }
    
    public class CustomConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(BaseClass).IsAssignableFrom(objectType) || typeof(BaseClass) == objectType;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            if (jo["Type"].Value<int>() == 0)
                return new BaseClass() { Type = jo["Type"].Value<int>()}; // avoid stack overflow

           if (jo["Type"].Value<int>() == 1)
               return jo.ToObject<DerviedClass>(serializer);
            return null;
        }

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

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
   }
}
Benzara Tahar
  • 2,058
  • 1
  • 17
  • 21