Deserializing different types using Newtonsoft JSON.NET

Jun 23, 2013 dotnet csharp json

I want to be able to serialize and deserialize different types in my datastore json. This is how you can do it in .NET using a custom SerializationBinder. Json.NET uses the .NET SerializationBinder to work out custom types.

This relies upon a $type field being added to objects in your JSON, but this is probably going to be OK since we don't need types for arrays or simple types (like string or int).

Lets start off with a test to explain what I'm doing:

[Test]
public void SimpleStepAreDeserializedIntoCorrectTypes()
{
    var knownTypesBinder = new KnownTypesBinder();
    knownTypesBinder.AddAssembly(Assembly.GetAssembly(typeof(KnownTypesBinder)));

    var json = JsonConvert.SerializeObject(_template, Formatting.Indented, new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.Objects,
        Binder = knownTypesBinder
    });

    var deserialized = JsonConvert.DeserializeObject(json, new JsonSerializerSettings
    {
        TypeNameHandling = TypeNameHandling.Objects,
        Binder = knownTypesBinder
    });

    Assert.IsInstanceOf(deserialized);
    Assert.IsInstanceOf(deserialized.Steps[0]);
    Assert.IsInstanceOf(deserialized.Steps[0].PostValidationRules[0]);
    Assert.IsInstanceOf(deserialized.Steps[1]);
    Assert.IsInstanceOf(((StoreNewItemStep)deserialized.Steps[1]).WhatToDo[0]);
}

Here's the implementation. Inspiration taken from the Type Converting thread and the Custom Serialization Binder page which this is based on.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;

namespace Flow.Library
{
    public class KnownTypesAssemblyBinder: SerializationBinder
    {
        private readonly IList _knownTypes = new List();

        public void AddAssembly(Assembly assembly)
        {
            foreach (var type in assembly.GetTypes())
            {
                _knownTypes.Add(type);
            }
        }

        public override Type BindToType(string assemblyName, string typeName)
        {
            var result = _knownTypes.SingleOrDefault(t => t.Name == typeName);
            return result;
        }

        public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
        {
            assemblyName = null;
            typeName = serializedType.Name;
        }
    }
}

This absolutely passes with no issues. Note the $type attribute in the example output below:

{
  "$type": "FlowTemplate",
  "Id": null,
  "Name": "Example Flow Template",
  "Version": 0,
  "Modified": "0001-01-01T00:00:00",
  "Author": null,
  "Tags": null,
  "Steps": [
    {
      "$type": "FormCollectionStepTemplate",
      "Form": "Step 1 Form",
      "Id": "Step 1 Id",
      "Version": 111,
      "Name": "Step 1 Name",
      "Author": "[email protected]",
      "Tags": null,
      "PreValidationRules": [],
      "PostValidationRules": [
        {
          "$type": "RegexValidationRule",
          "IsValid": false
        }
      ]
    },
    {
      "$type": "StoreNewItemStep",
      "WhatToDo": [
        {
          "$type": "StoreWhatWhere",
          "SourceKey": "name",
          "Collection": "customer",
          "Key": "name"
        },
        {
          "$type": "StoreWhatWhere",
          "SourceKey": "email",
          "Collection": "customer",
          "Key": "email"
        }
      ],
      "Id": "Step 2 Id",
      "Version": 222,
      "Name": "Store Customer Information",
      "Author": "[email protected]",
      "Tags": null,
      "PreValidationRules": [],
      "PostValidationRules": []
    }
  ],
  "Links": [],
  "Groups": []
}