Home > Mobile >  Deserializing a a dynamic type with custom object creation with Newtonsoft.Json
Deserializing a a dynamic type with custom object creation with Newtonsoft.Json

Time:01-03

So let's say I have a:

List<IInterface> list;

that has been serialized with TypeNameHandling.Auto, so it has "dynamic" type information. I can deserialize it fine as Newtonsoft.Json can recognize the type from the $type and Json can use the correct constructor. So far so good.

Now say I want to override the creation converter with a mehtod:

CustomCreationConverter<IInterface>

that overrides the creation of the object:

public override IInterface Create(Type objectType)

At this point objectType will always be IInterface and not a derived implementation, so I have no way to create the correct object. The meta-information of $type is now lost.

Is there an elegant way to fix this?

Here would be an attempt that does not work:

public class CustomConverter : CustomCreationConverter<Example.IInterface> {
    public override Example.IInterface Create(Type objectType) {
        return Example.MakeObject(objectType); // this won't work, objectType will always be IInterface
    }
}

public class Example {
    public interface IInterface { };
    public class A : IInterface { public int content; };
    public class B : IInterface { public float data; };
    public static IInterface MakeObject(Type t) {
        if (t == typeof(IInterface)) {
            throw new Exception();
        }
        return t == typeof(A) ? new A() : new B();
    }

    public static void Serialize() {
        var settings = new JsonSerializerSettings() {
            TypeNameHandling = TypeNameHandling.Auto
        };

        JsonSerializer serializer = JsonSerializer.Create(settings);
        // serializer.Converters.Add(new CustomConverter()); // ?? can't have both, either CustomConverter or $type
        List<IInterface> list = new() { MakeObject(typeof(A)), MakeObject(typeof(B)) };

        using (StreamWriter sw = new("example.json")) {
            serializer.Serialize(sw, list);
        }

        // Now read back example.json into a List<IInterface> using MakeObject

        // Using CustomConverter won't work
        using (JsonTextReader rd = new JsonTextReader(new StreamReader("example.json"))) {
            List<IInterface> list2 = serializer.Deserialize<List<IInterface>>(rd);
        }
    }

}

CodePudding user response:

Once you provide a custom converter such as CustomCreationConverter<T> for a type, the converter is responsible for all the deserialization logic including logic for type selection logic that would normally be implemented by TypeNameHandling. If you only want to inject a custom factory creation method and leave all the rest of the deserialization logic unchanged, you could create your own custom contract resolver and inject the factory method as JsonContract.DefaultCreator.

To implement this, first define the following factory interface and contract resolver:

public interface IObjectFactory<out T>
{
    bool CanCreate(Type type);
    T Create(Type type);
}

public class ObjectFactoryContractResolver : DefaultContractResolver
{
    readonly IObjectFactory<object> factory;
    public ObjectFactoryContractResolver(IObjectFactory<object> factory) => this.factory = factory ?? throw new ArgumentNullException(nameof(factory));

    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        if (factory.CanCreate(objectType))
        {
            contract.DefaultCreator = () => factory.Create(objectType);
            contract.DefaultCreatorNonPublic = false;
        }
        return contract;
    }
}

Next, refactor your IInterface class hierarchy to make use of an IObjectFactory as an object creation factory:

public class InterfaceFactory : IObjectFactory<IInterface>
{
    public InterfaceFactory(string runtimeId) => this.RuntimeId = runtimeId; // Some value to inject into the constructor
    string RuntimeId { get; }
    
    public bool CanCreate(Type type) => !type.IsAbstract && typeof(IInterface).IsAssignableFrom(type);
    public IInterface Create(Type type) => type switch
        {
            var t when t == typeof(A) => new A(RuntimeId),
            var t when t == typeof(B) => new B(RuntimeId),
            _ => throw new NotImplementedException(type.ToString()),
        };
}

public interface IInterface
{
    public string RuntimeId { get; }
}

public class A : IInterface
{
    [JsonIgnore] public string RuntimeId { get; }
    internal A(string id) => this.RuntimeId = id;
    public int content { get; set; }
}

public class B : IInterface
{
    [JsonIgnore] public string RuntimeId { get; }
    internal B(string id) => this.RuntimeId = id;
    public float data { get; set; }
}

(Here RuntimeId is some value that needs to be injected during object creation.)

Now you will be able to construct your list as follows:

var valueToInject = "some value to inject";
var factory = new InterfaceFactory(valueToInject);
List<IInterface> list = new()  { factory.Create(typeof(A)), factory.Create(typeof(B)) };

And serialize and deserialize as follows:

var resolver = new ObjectFactoryContractResolver(factory)
{
    // Set any necessary properties e.g.
    NamingStrategy = new CamelCaseNamingStrategy(),
};
var settings = new JsonSerializerSettings
{
    ContractResolver = resolver,
    TypeNameHandling = TypeNameHandling.Auto,
};

var json = JsonConvert.SerializeObject(list, Formatting.Indented, settings);

var list2 = JsonConvert.DeserializeObject<List<IInterface>>(json, settings);

Notes:

Demo fiddle here.

  • Related