简体   繁体   中英

Deserializing a a dynamic type with custom object creation with Newtonsoft.Json

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);
        }
    }

}

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 .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM