JsonConverter and object type hints

Dec 30, 2010 at 4:42 PM

Hi, I'm wondering if someone can give me a little guidance with the following problem.  I need to deserialize some objects coming from a Java project (aka I do not have control over the JSON structure).  Some of the objects I'm retrieving have abstract members with type codes in the JSON structure so that you know what type to expect when unmarshalling.  A simplified version of what I need looks like this:

public abstract class Pet
{
    public string Discriminator { get; protected set; }

    protected Pet(string discriminator)
    {
        Discriminator = discriminator;
    }
}

public class Dog : Pet
{
    public Dog() : base("DOG") { }
}
public class Cat : Pet
{
    public Cat() : base("CAT") { }
}

public class Owner
{
    public Pet Pet { get; set; }
}

A serialized (Cat) Owner would look like {"Pet":{"Discriminator":"CAT"}}

Basically, when a Pet field is deserialized, I need to look-ahead into the Pet structure to determine which type to instantiate.  This could be accomplished with a JsonConverter:

public class PetConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) ...

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject obj = JObject.Load(reader);
        string discriminator = (string)obj["Discriminator"];

        Pet pet;
        switch (discriminator)
        {
            case "CAT":
                pet = new Cat();
                break;
            case "DOG":
                pet = new Dog();
                break;
            default:
                throw new NotImplementedException();
        }

        serializer.Populate(reader, pet);  // Won't work, as reader has been moved

        return pet;
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Pet).IsAssignableFrom(objectType);
    }
}
The problem here is that JsonReader is one way and is forwarded past the current object at JObject.Load(reader). The only solution I can see to this would be either cloning the reader (probably not a good idea) or adding mark/reset support to JsonReader (not sure how tough that would be but I'll look into it).  Many Java Readers have this ability and I think it could work in a similar fashion. 
Another idea I was playing around with was adding some more control to TypeNameHandling.  For example adding a TypeNameHandling.Custom which would look at a class attribute like [TypeNameKey("CAT")].  The trouble with this system would be that you would either have to scan for implementing classes with TypeNameKey, or register them against JsonSerializerSettings.  The other would be to add all the hints to the abstract class/interface but I don't like that 'cause that creates a bad dependency between them.
Thoughts/Comments?  Can anyone see a solution that wouldn't involve changes to Json.NET?  If James/the community likes the adding mark/reset support to JsonReader idea I can probably propose a patch.
Thanks
Mark 
Dec 30, 2010 at 10:10 PM
Edited Dec 30, 2010 at 10:13 PM

Ok, found an interesting solution without making changes to Json.NET.  You can obtain JsonReaders from JsonObjects.  My example PetConverter needs only slight modification to use this:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    JObject obj = JObject.Load(reader);
    string discriminator = (string)obj["Discriminator"];

    Pet pet;
    switch (discriminator)
    {
        case "CAT":
            pet = new Cat();
            break;
        case "DOG":
            pet = new Dog();
            break;
        default:
            throw new NotImplementedException();
    }

    serializer.Populate(obj.CreateReader(), pet);

    return pet;
}

Note the only difference here is that I create a new JsonReader from my JObject rather than using the (now repositioned) reader.  Apologies if this pattern has been suggested here before but I didn't see it in any of the forum posts.

Thanks

Mark

May 4, 2015 at 2:27 PM
Edited May 4, 2015 at 4:07 PM
I needed the exact same thing. This works beautifully.

Here's a generic version you can set up using fluent mapping. It even goes both ways, serializing the discriminator property when writing.

Usage:
serializer.Converters.Add(new DerivedDiscriminatorConverter<InputConfigBase>("type")
    .Map<VoltageSeriesInputConfig>("voltage-series")
    .Map<CircuitTransformerInputConfig>("current-transformer")
    .Map<SingleVoltageInputConfig>("single-voltage")
    .Map<DigitalInputConfig>("digital"));