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:
- For serialization I need to bind a component type (i.e. class) to a type alias.
- For deserialization I need to bind the type alias to a service type (i.e. interface).
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:
- Instead of using
new JsonSerializer {...};I used theJsonSerializer.Createfactory method, which takesJsonSerializerSettingsas a parameter, to avoid redundant settings specifications. I don't think this should have any impact, but I wanted to mention it. - I added my
CustomSerializationBinderto thesettings, 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:
- The
DefaultCreatordelegate 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:
- The mappings were changed according to my requirements:
TypeToAliasMappinguses the component type (i.e. class) for serializing.AliasToTypeMappinguses 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:
- I added specific interfaces for each class which all share the common interface,
IWidget, used inList<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?