If we have an object where one of the properties is an interface, with several possible concrete implementations, how can we simply serialize and deserialize the object with Json.NET?

Take the example of a Garage that sells vehicles:

public class Garage
{
    public IEnumerable<IVehicle> Stock { get; set; }

    public Manager Manager { get; set; }
}

 

The vehicles we sell include cars and buses:

public class Car : IVehicle
{
    public string Make { get; set; }
    public string Colour { get; set; }
}

public class Bus : IVehicle
{
    public string Brand { get; set; }
    public int Length { get; set; }
}

 

Where IVehicle is just used as a marker interface:

public interface IVehicle { }

 

For good measure, we also have a garage manager:

public class Manager
{
    public string Name { get; set; }
}

 

We now create some stock for our garage:

IVehicle car = new Car
{
    Make = "Ford",
    Colour = "Red"
};

IVehicle bus = new Bus
{
    Brand = "Leyland",
    Length = 20
};

var garage = new Garage
{
    Manager = new Manager { Name = "Steve" },
    Stock = new List<IVehicle> { car, bus }
};

 

Using default settings we try to serialize the object:

string defaultJson = JsonConvert.SerializeObject(garage);

 

The JSON produced looks ok on first inspection:

{
	"Stock":
		[{"Make":"Ford","Colour":"Red"},
		 {"Brand":"Leyland","Length":20}],
	"Manager":{"Name":"Steve"}
}

 

But if we then try to deserialize that:

Garage result = JsonConvert.DeserializeObject<Garage>(defaultJson);

 

We have a problem:

Newtonsoft.Json.JsonSerializationException: 'Could not create an instance
of type Serialization.IVehicle. Type is an interface or abstract class and
cannot be instantiated. Path 'Stock[0].Make', line 1, position 18.'

 

In other words, Json.NET has no way of knowing the concrete classes each type of vehicle should be deserialized into.

To fix this, we need to include some sort of discriminator field in the JSON, from which we can get the concrete type when deserializing.

Json.NET offers a possible solution:

string json = JsonConvert.SerializeObject(garage, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Auto
});

 

This instructs the serializer to add a discriminator when the type isn’t obvious (note that as Manager is a concrete class it does not need a discriminator):

{
	"Stock": [{
		"$type": "Serialization.Car, Serialization",
		"Make": "Ford",
		"Colour": "Red"
	},
	{
		"$type": "Serialization.Bus, Serialization",
		"Brand": "Leyland",
		"Length": 20
	}],
	"Manager": {
		"Name": "Steve"
	}
}

 

That’s looking better and will happily deserialize if we use the same settings:

Garage result = JsonConvert.DeserializeObject<Garage>(json, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Auto
});

 

But we still have a possible future deserialization problem.  The discriminator value is the full type name (in my case both the assembly and namespace are “Serialization”).  What if we refactor the code and the namespace changes?  We will be unable to deserialize any existing JSON afterwards.

One way of fixing this is to override the discriminator name, just to use the type-name.

First we need a custom ISerializationBinder:

public class KnownTypesBinder : ISerializationBinder
{
    public IList<Type> KnownTypes { get; set; }

    public Type BindToType(string assemblyName, string typeName)
    {
        return KnownTypes.SingleOrDefault(t => t.Name == typeName);
    }

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

 

Then we use this for the vehicle classes:

string json = JsonConvert.SerializeObject(garage, new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Auto,
    SerializationBinder = new KnownTypesBinder
    {
        KnownTypes = new List<Type>
        {
            typeof(Car),
            typeof(Bus)
        }
    }
});

 

The JSON produced is:

{
	"Stock": [{
		"$type": "Car",
		"Make": "Ford",
		"Colour": "Red"
	},
	{
		"$type": "Bus",
		"Brand": "Leyland",
		"Length": 20
	}],
	"Manager": {
		"Name": "Steve"
	}
}

Provided we then deserialize using the same settings, this solves the problem.

There are other way of tackling this problem, such as using a custom JsonConverter to add your own discriminator, then having the converter instantiate whichever concrete class is required.  But in a system where you are in control of both the serialization and deserialization, the approach above is much simpler.

Comments


Comments are closed