Building on this very helpful answer from Brian Rogers I wrote the this, but it throws an exception, see more below.
I re-wrote the code, because the original version unfortunately doesn't quite meet my requirements: I need to include an ISerializationBinder implementation, because I have to map types differently for use with an IOC ("Inversion Of Control") container:
Below is my code, including comments on what I changed compared to Brian's original answer.
Main:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public class Program
{
public static void Main()
{
Assembly thing = new Assembly
{
Name = "Gizmo",
Parts = new List<IWidget>
{
new Assembly
{
Name = "Upper Doodad",
Parts = new List<IWidget>
{
new Bolt { Name = "UB1", HeadSize = 2, Length = 6 },
new Washer { Name = "UW1", Thickness = 1 },
new Nut { Name = "UN1", Size = 2 }
}
},
new Assembly
{
Name = "Lower Doodad",
Parts = new List<IWidget>
{
new Bolt { Name = "LB1", HeadSize = 3, Length = 5 },
new Washer { Name = "LW1", Thickness = 2 },
new Washer { Name = "LW2", Thickness = 1 },
new Nut { Name = "LN1", Size = 3 }
}
}
}
};
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
Formatting = Formatting.Indented,
ContractResolver = new CustomResolver(),
SerializationBinder = new CustomSerializationBinder(),
};
Console.WriteLine("----- Serializing widget tree to JSON -----");
string json = JsonConvert.SerializeObject(thing, settings);
Console.WriteLine(json);
Console.WriteLine();
Console.WriteLine("----- Deserializing JSON back to widgets -----");
using (MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
using (StreamReader sr = new StreamReader(stream))
using (JsonTextReader reader = new JsonTextReader(sr))
{
var serializer = JsonSerializer.Create(settings);
var widget = serializer.Deserialize<IAssembly>(reader);
Console.WriteLine();
Console.WriteLine(widget.ToString(""));
}
}
}
Changes:
new JsonSerializer {...}; I used the JsonSerializer.Create factory method, which takes JsonSerializerSettings as a parameter, to avoid redundant settings specifications. I don't think this should have any impact, but I wanted to mention it.CustomSerializationBinder to the settings, used for both deserialization and serialization.Contract Resolver:
public class CustomResolver : DefaultContractResolver
{
private MockIoc _ioc = new MockIoc();
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
JsonObjectContract contract = base.CreateObjectContract(objectType);
// This is just to show that the CreateObjectContract is being called
Console.WriteLine("Created a contract for '" + objectType.Name + "'.");
contract.DefaultCreator = () =>
{
// Use your IOC container here to create the instance.
var instance = _ioc.Resolve(objectType);
// This is just to show that the DefaultCreator is being called
Console.WriteLine("Created a '" + objectType.Name + "' instance.");
return instance;
};
return contract;
}
}
Changes:
DefaultCreator delegate now uses a mock IOC, see below, to create the required instance.Serialization Binder:
public class CustomSerializationBinder : ISerializationBinder
{
Dictionary<string, Type> AliasToTypeMapping { get; }
Dictionary<Type, string> TypeToAliasMapping { get; }
public CustomSerializationBinder()
{
TypeToAliasMapping = new Dictionary<Type, string>
{
{ typeof(Assembly), "A" },
{ typeof(Bolt), "B" },
{ typeof(Washer), "W" },
{ typeof(Nut), "N" },
};
AliasToTypeMapping = new Dictionary<string, Type>
{
{ "A", typeof(IAssembly) },
{ "B", typeof(IBolt) },
{ "W", typeof(IWasher) },
{ "N", typeof(INut) },
};
}
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
var alias = TypeToAliasMapping[serializedType];
assemblyName = null; // I don't care about the assembly name for this example
typeName = alias;
Console.WriteLine("Binding type " + serializedType.Name + " to alias " + alias);
}
public Type BindToType(string assemblyName, string typeName)
{
var type = AliasToTypeMapping[typeName];
Console.WriteLine("Binding alias " + typeName + " to type " + type);
return type;
}
}
Changes:
TypeToAliasMapping uses the component type (i.e. class) for serializing.AliasToTypeMapping uses the service type (i.e. interface) for deserializing.Mock IOC Container:
public class MockIoc
{
public Dictionary<Type, Type> ServiceToComponentMapping { get; set; }
public MockIoc()
{
ServiceToComponentMapping = new Dictionary<Type, Type>
{
{ typeof(IAssembly), typeof(Assembly) },
{ typeof(IBolt) , typeof(Bolt) },
{ typeof(IWasher) , typeof(Washer) },
{ typeof(INut) , typeof(Nut) },
};
}
public object Resolve(Type serviceType)
{
var componentType = ServiceToComponentMapping[serviceType];
var instance = Activator.CreateInstance(componentType);
return instance;
}
}
This is new and simply maps the interface (obtained by the binding during deserialization) to a class (used for creating an actual instance).
Example classes:
public interface IWidget
{
string Name { get; }
string ToString(string indent = "");
}
public interface IAssembly : IWidget { }
public class Assembly : IAssembly
{
public string Name { get; set; }
public List<IWidget> Parts { get; set; }
public string ToString(string indent = "")
{
var sb = new StringBuilder();
sb.AppendLine(indent + "Assembly " + Name + ", containing the following parts:");
foreach (IWidget part in Parts)
{
sb.AppendLine(part.ToString(indent + " "));
}
return sb.ToString();
}
}
public interface IBolt : IWidget { }
public class Bolt : IBolt
{
public string Name { get; set; }
public int HeadSize { get; set; }
public int Length { get; set; }
public string ToString(string indent = "")
{
return indent + "Bolt " + Name + " , head size + " + HeadSize + ", length "+ Length;
}
}
public interface IWasher : IWidget { }
public class Washer : IWasher
{
public string Name { get; set; }
public int Thickness { get; set; }
public string ToString(string indent = "")
{
return indent+ "Washer "+ Name + ", thickness " + Thickness;
}
}
public interface INut : IWidget { }
public class Nut : INut
{
public string Name { get; set; }
public int Size { get; set; }
public string ToString(string indent = "")
{
return indent + "Nut " +Name + ", size " + Size;
}
}
Changes:
IWidget, used in List<IWidget> Assembly.Parts.Output:
----- Serializing widget tree to JSON -----
Created a contract for 'Assembly'.
Binding type Assembly to alias A
Created a contract for 'IWidget'.
Binding type Assembly to alias A
Created a contract for 'Bolt'.
Binding type Bolt to alias B
Created a contract for 'Washer'.
Binding type Washer to alias W
Created a contract for 'Nut'.
Binding type Nut to alias N
Binding type Assembly to alias A
Binding type Bolt to alias B
Binding type Washer to alias W
Binding type Washer to alias W
Binding type Nut to alias N
{
"$type": "A",
"Name": "Gizmo",
"Parts": [
{
"$type": "A",
"Name": "Upper Doodad",
"Parts": [
{
"$type": "B",
"Name": "UB1",
"HeadSize": 2,
"Length": 6
},
{
"$type": "W",
"Name": "UW1",
"Thickness": 1
},
{
"$type": "N",
"Name": "UN1",
"Size": 2
}
]
},
{
"$type": "A",
"Name": "Lower Doodad",
"Parts": [
{
"$type": "B",
"Name": "LB1",
"HeadSize": 3,
"Length": 5
},
{
"$type": "W",
"Name": "LW1",
"Thickness": 2
},
{
"$type": "W",
"Name": "LW2",
"Thickness": 1
},
{
"$type": "N",
"Name": "LN1",
"Size": 3
}
]
}
]
}
----- Deserializing JSON back to widgets -----
Created a contract for 'IAssembly'.
Binding alias A to type IAssembly
Created a 'IAssembly' instance.
Run-time exception (line 179): Object reference not set to an instance of an object.
Stack Trace:
[System.NullReferenceException: Object reference not set to an instance of an object.]
at Assembly.ToString(String indent) :line 179
at Program.Main() :line 68
Serialization appears to be fully correct, but the exception and console outputs indicate that while the "root" Assembly instance was created correctly, its members were not populated.
As mentioned in my previous question, I'm currently using a JsonConverter in combination with a SerializationBinder - and it works fine, except for the performance (see link above). It's been a while since I implemented that solution and I remember finding it hard to wrap my head around the intended or actual data flow, esp. while deserializing. And, likewise, I don't yet quite understand how the ContractResolver comes into play here.
What am I missing?
This is very close to working. The problem is that you have made your IWidget derivative interfaces empty. Since the SerializationBinder is binding the $type codes in the JSON to interfaces instead of concrete types, the contract resolver is going to look at the properties on those interfaces to determine what can be deserialized. (The contract resolver's main responsibility is to map properties in the JSON to their respective properties in the class structure. It assigns a "contract" to each type, which governs how that type is handled during de/serialization: how it is created, whether to apply a JsonConverter to it, and so forth.)
IAssembly doesn't have a Parts list on it, so that's as far as the deserialization gets. Also, the Name property on IWidget does not have a setter, so the name of the root assembly object doesn't get populated either. The NRE is being thrown in the ToString() method in the demo code which is trying to dump out the null Parts list without an appropriate null check (that one's on me). Anyway, if you add the public properties from the concrete Widget types to their respective interfaces, then it works. So:
public interface IWidget
{
string Name { get; set; }
string ToString(string indent = "");
}
public interface IAssembly : IWidget
{
List<IWidget> Parts { get; set; }
}
public interface IBolt : IWidget
{
int HeadSize { get; set; }
int Length { get; set; }
}
public interface IWasher : IWidget
{
public int Thickness { get; set; }
}
public interface INut : IWidget
{
public int Size { get; set; }
}
Here's the working Fiddle: https://dotnetfiddle.net/oJ54VD
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With