Search code examples
c#json.netdeserialization

NewtonSoft Inheretance deserialization of an JSON array Without altering the JSON


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?


Solution

  • 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;}
        }