using Newtonsoft.Json.Linq;
using System.Collections.Generic;
[JsonConverter(typeof(JsonSubtypes))]
[JsonTag, JsonProperty("@type")]
public string Type {get;set;}
public abstract class Shape: ShapeBase {
public abstract double GetArea();
public class Circle: Shape {
[JsonProperty("super-radius")]
public double Radius { get; set; }
public override double GetArea() {
return Radius * Radius * Math.PI;
[JsonSubtype("rectangle")]
public class Rectangle: Shape {
public double Height { get; set; }
public double Width { get; set; }
public override double GetArea() {
public class Group: Shape {
public List<Shape> Items { get; set; }
public override double GetArea() {
return Items.Select(item => item.GetArea()).Sum();
public static void Main()
JsonSubtypes.autoRegister(Assembly.GetExecutingAssembly());
Shape original = new Group() {
Items = new List<Shape> {
new Circle() { Radius = 5 },
new Rectangle() { Height = 10, Width = 20 }
string str = JsonConvert.SerializeObject(original);
var copy = JsonConvert.DeserializeObject(str,typeof(Shape)) as Shape;
Console.WriteLine("original.area = {0}, copy.area = {1}", original.GetArea(), copy.GetArea());
public class JsonSubtypeClashException: Exception {
public string TagValue { get; private set;}
public Type RootType { get; private set; }
public Type OldType { get; private set; }
public Type NewType { get; private set; }
public JsonSubtypeClashException(Type rootType, string tagValue, Type oldType, Type newType): base(
"JsonSubtype Clash for {0}[tag={1}]: oldType = {2}, newType = {3}",
public class JsonSubtypeNoRootException: Exception {
public Type SubType { get; private set; }
public JsonSubtypeNoRootException(Type subType): base(
"{0} should be inherited from the class with the [JsonConverter(typeof(JsonSubtypes))] attribute",
public class JsonSubtypeNoTagException: Exception {
public Type SubType { get; private set; }
public JsonSubtypeNoTagException(Type subType): base(
@"{0} should have [JsonSubtype(""..."")] attribute",
public class JsonSubtypeNotRegisteredException: Exception {
public Type Root { get; private set; }
public string TagValue { get; private set; }
public JsonSubtypeNotRegisteredException(Type root, string tagValue): base(
@"Unknown tag={1} for class {0}",
[AttributeUsage(AttributeTargets.Class)]
public class JsonSubtypeAttribute: Attribute {
public JsonSubtypeAttribute(string tagValue) {
this.tagValue = tagValue;
public static class JsonSubtypesExtension {
public static bool TryGetAttribute<T>(this Type t, out T attribute) where T: Attribute {
attribute = t.GetCustomAttributes(typeof(T), false).Cast<T>().FirstOrDefault();
return attribute != null;
private static Dictionary<Type, PropertyInfo> tagProperties = new Dictionary<Type, PropertyInfo>();
public static bool TryGetTagProperty(this Type t, out PropertyInfo tagProperty) {
if (!tagProperties.TryGetValue(t, out tagProperty)) {
JsonConverterAttribute conv;
if (t.TryGetAttribute(out conv) && conv.ConverterType == typeof(JsonSubtypes)) {
var props = (from prop in t.GetProperties() where prop.GetCustomAttribute(typeof(JsonTagAttribute)) != null select prop).ToArray();
if (props.Length == 0) throw new Exception("No tag");
if (props.Length > 1) throw new Exception("Multiple tags");
tagProperties[t] = tagProperty;
return tagProperty != null;
public static bool TryGetTagValue(this Type t, out string tagValue) {
JsonSubtypeAttribute subtype;
if (t.TryGetAttribute(out subtype)) {
tagValue = subtype.TagValue;
public static bool TryGetJsonRoot(this Type t, out Type root, out PropertyInfo tagProperty) {
if (root.TryGetTagProperty(out tagProperty)) {
public class JsonTagAttribute: Attribute {
public class JsonTagInfo {
public PropertyInfo Property { get; set; }
public string Value { get; set; }
public class JsonRootInfo {
public PropertyInfo Property { get; set; }
public Type Root { get; set; }
public abstract class DefaultJsonConverter: JsonConverter {
private static bool silentWrite;
private static bool silentRead;
public sealed override bool CanWrite {
var canWrite = !silentWrite;
public sealed override bool CanRead {
var canRead = !silentRead;
protected void _WriteJson(JsonWriter writer, Object value, JsonSerializer serializer) {
serializer.Serialize(writer, value);
protected Object _ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) {
return serializer.Deserialize(reader, objectType);
public class JsonSubtypes: DefaultJsonConverter {
private static Dictionary<Type, Dictionary<string, Type>> implementations = new Dictionary<Type, Dictionary<string, Type>>();
private static Dictionary<Type, JsonTagInfo> tags = new Dictionary<Type, JsonTagInfo>();
private static Dictionary<Type, JsonRootInfo> roots = new Dictionary<Type, JsonRootInfo>();
public static void register(Type newType) {
PropertyInfo tagProperty;
if (newType.TryGetJsonRoot(out root, out tagProperty)) {
for(var t = newType; t != root; t = t.BaseType) {
roots[t] = new JsonRootInfo() {
roots[root] = new JsonRootInfo() {
Dictionary<string, Type> implementationMap;
if (!implementations.TryGetValue(root, out implementationMap)) {
implementationMap = new Dictionary<string, Type>();
implementations[root] = implementationMap;
JsonSubtypeAttribute attr;
if (!newType.TryGetAttribute(out attr)) {
throw new JsonSubtypeNoTagException(newType);
var tagValue = attr.TagValue;
if (implementationMap.TryGetValue(tagValue, out oldType)) {
throw new JsonSubtypeClashException(root, tagValue, oldType, newType);
implementationMap[tagValue] = newType;
tags[newType] = new JsonTagInfo() {
throw new JsonSubtypeNoRootException(newType);
public static void autoRegister(Assembly assembly) {
foreach(var type in assembly.GetTypes().Where(type => type.GetCustomAttribute<JsonSubtypeAttribute>() != null)) {
public override bool CanConvert(Type t) {
public static T EnsureTag<T>(T value) {
if (tags.TryGetValue(value.GetType(), out tagInfo)) {
tagInfo.Property.SetValue(value, tagInfo.Value);
public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer) {
_WriteJson(writer, EnsureTag(value), serializer);
public override Object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) {
if (tags.TryGetValue(objectType, out tagInfo)) {
return _ReadJson(reader, objectType, existingValue, serializer);
if (roots.TryGetValue(objectType, out rootInfo)) {
JToken t = JToken.ReadFrom(reader);
var stub = _ReadJson(t.CreateReader(), rootInfo.Root, existingValue, serializer);
var tagValue = rootInfo.Property.GetValue(stub) as string;
var implementationMap = implementations[rootInfo.Root];
if (implementationMap.TryGetValue(tagValue, out implementation)) {
return ReadJson(t.CreateReader(), implementation, null, serializer);
throw new JsonSubtypeNotRegisteredException(rootInfo.Root, tagValue);
return _ReadJson(reader, objectType, existingValue, serializer);
public static T Deserialize<T>(string s) where T: class {
return JsonConvert.DeserializeObject(s, typeof(T)) as T;
public static string Serialize<T>(T value) where T: class {
return JsonConvert.SerializeObject(value);