0

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

}
FilippoL
  • 160
  • 7
  • 1
    Can you please add a [mre]? – Guru Stron Jan 02 '23 at 18:41
  • By design, a custom creation converter is going to work at cross-purposes to `TypeNameHandling.Auto`. The former is responsible for manual construction, the latter tells Json.NET how to construct things automatically. Can you share exactly how you want to combine these two functionalities? If you're looking to inject some custom logic for parsing the `$type` value, the intended method is via a [Custom SerializationBinder](https://www.newtonsoft.com/json/help/html/SerializeSerializationBinder.htm). See e.g. [Custom $type value for serialized objects](https://stackoverflow.com/q/49283251). – dbc Jan 02 '23 at 18:55
  • Say I still want to make use of the type information, but instead I have my own factory that given the Type returns an appropriate object of that Type (for which I cannot use the constructor). ` class A : IInterface {}; class B : IInterface{}; IInterface MakeObject(Type t) { [code] } List list = new List{MakeObject(typeof(A)), MakeObject(typeof(B))} [serialize list with type info] ` – FilippoL Jan 02 '23 at 18:58
  • In that case I'd probably go for a [custom contract resolver](https://www.newtonsoft.com/json/help/html/contractresolver.htm#CustomIContractResolverExamples) and inject the factory method as [`JsonContract.DefaultCreator`](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_Serialization_JsonContract_DefaultCreator.htm). That way your factory code won't interfere with `TypeNameHandling.Auto`. Does that work for you? If so, I could add an answer if you [edit] your question to include a basic [mcve]. – dbc Jan 02 '23 at 19:02
  • I've added an example. – FilippoL Jan 02 '23 at 19:18
  • @FilippoL " I want to override the creation converter" why do you want ? what is the problem? What are you trying to get in the end? – Serge Jan 02 '23 at 19:32
  • I can't use the constructor for the types, still I wan't to create them approrpiately using the saved type. – FilippoL Jan 02 '23 at 19:42

1 Answers1

1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340