Serialización de tipo Json.Net con un objeto secundario polimórfico

Nos gustaría poder serializar / deserializar json de / a clases C #, con la clase principal teniendo una instancia de un objeto secundario polimórfico. Hacerlo es fácil usando la configuración TypeNameHandling.Auto de Json.Net. Sin embargo, nos gustaría hacerlo sin el campo “$ type”.

Lo primero que se debe hacer es cambiar el nombre de “$ type” por el valor que elijamos y que el valor para el tipo sea una enumeración que correlacione correctamente los subtipos. No lo he visto como una opción, pero me alegraría saber si es posible.

El segundo pensamiento fue a lo largo de las siguientes líneas … A continuación se muestra un primer paso en las clases, con la clase de nivel superior que tiene un indicador (SubTypeType) en cuanto a qué tipo de datos está contenido en el objeto secundario (SubTypeData). He profundizado un poco en la documentación de Json.Net y he intentado algunas cosas pero no he tenido suerte.

Actualmente tenemos un control total sobre la definición de datos, pero una vez que se implementa, las cosas se bloquean.

public class MainClass { public SubType SubTypeType { get; set; } public SubTypeClassBase SubTypeData { get; set; } } public class SubTypeClassBase { } public class SubTypeClass1 : SubTypeClassBase { public string AaaField { get; set; } } public class SubTypeClass2 : SubTypeClassBase { public string ZzzField { get; set; } } 

Tener la información del subtipo en la clase contenedor es problemático por dos razones:

  1. La instancia de clase contenedor no es accesible cuando Json.NET lee la clase contenida.
  2. Si más adelante necesita convertir la propiedad SubTypeClassBase en, por ejemplo, una lista, no habrá ningún lugar para colocar la información del subtipo.

En cambio, recomendaría agregar la información del subtipo como una propiedad en la clase base:

 [JsonConverter(typeof(SubTypeClassConverter))] public class SubTypeClassBase { [JsonConverter(typeof(StringEnumConverter))] // Serialize enums by name rather than numerical value public SubType Type { get { return typeToSubType[GetType()]; } } } 

Ahora el enum del subtipo personalizado se serializará cada vez que se SubTypeClassBase un objeto asignable a SubTypeClassBase . Una vez hecho esto, para la deserialización puede crear un JsonConverter que cargue el json para un SubTypeClassBase dado en un JObject temporal, verifique el valor de la propiedad "Type" y deserialice el objeto JSON como la clase apropiada.

Implementación del prototipo a continuación:

 public enum SubType { BaseType, Type1, Type2, } [JsonConverter(typeof(SubTypeClassConverter))] public class SubTypeClassBase { static readonly Dictionary typeToSubType; static readonly Dictionary subTypeToType; static SubTypeClassBase() { typeToSubType = new Dictionary() { { typeof(SubTypeClassBase), SubType.BaseType }, { typeof(SubTypeClass1), SubType.Type1 }, { typeof(SubTypeClass2), SubType.Type2 }, }; subTypeToType = typeToSubType.ToDictionary(pair => pair.Value, pair => pair.Key); } public static Type GetType(SubType subType) { return subTypeToType[subType]; } [JsonConverter(typeof(StringEnumConverter))] // Serialize enums by name rather than numerical value public SubType Type { get { return typeToSubType[GetType()]; } } } public class SubTypeClass1 : SubTypeClassBase { public string AaaField { get; set; } } public class SubTypeClass2 : SubTypeClassBase { public string ZzzField { get; set; } } public class SubTypeClassConverter : JsonConverter { public override bool CanConvert(Type objectType) { return objectType == typeof(SubTypeClassBase); } public override bool CanWrite { get { return false; } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var token = JToken.Load(reader); var typeToken = token["Type"]; if (typeToken == null) throw new InvalidOperationException("invalid object"); var actualType = SubTypeClassBase.GetType(typeToken.ToObject(serializer)); if (existingValue == null || existingValue.GetType() != actualType) { var contract = serializer.ContractResolver.ResolveContract(actualType); existingValue = contract.DefaultCreator(); } using (var subReader = token.CreateReader()) { // Using "populate" avoids infinite recursion. serializer.Populate(subReader, existingValue); } return existingValue; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } 

Puede probar con la implementación del convertidor JsonSubtypes que admite el mapeo de tipo de registro con valores enum.

En tu caso, se ve así:

  public class MainClass { public SubTypeClassBase SubTypeData { get; set; } } [JsonConverter(typeof(JsonSubtypes), "SubTypeType")] [JsonSubtypes.KnownSubType(typeof(SubTypeClass1), SubType.WithAaaField)] [JsonSubtypes.KnownSubType(typeof(SubTypeClass2), SubType.WithZzzField)] public class SubTypeClassBase { public SubType SubTypeType { get; set; } } public class SubTypeClass1 : SubTypeClassBase { public string AaaField { get; set; } } public class SubTypeClass2 : SubTypeClassBase { public string ZzzField { get; set; } } public enum SubType { WithAaaField, WithZzzField } [TestMethod] public void Deserialize() { var obj = JsonConvert.DeserializeObject("{\"SubTypeData\":{\"ZzzField\":\"zzz\",\"SubTypeType\":1}}"); Assert.AreEqual("zzz", (obj.SubTypeData as SubTypeClass2)?.ZzzField); } 

Aquí hay un ejemplo completo que puede leer y escribir JSON con objetos polimórficos.

Suponiendo que tenemos la siguiente estructura de clase:

 public class Base {} public class SubClass1 : Base { public int field1; } public class SubClass2 : Base { public int field2; } 

Podemos usar un convertidor personalizado que crea un campo adicional en el type nombre JSON al serializarlo y lo lee cuando deserializa.

 public class PolymorphicJsonConverter : JsonConverter { public override object ReadJson (JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { JObject item = JObject.Load(reader); var type = item["type"].Value(); if (type == "SubClass1") { return item.ToObject(); } else if (type == "SubClass2") { return item.ToObject(); } else { return null; } } public override void WriteJson (JsonWriter writer, object value, JsonSerializer serializer) { JObject o = JObject.FromObject(value); if (value is SubClass1) { o.AddFirst(new JProperty("type", new JValue("SubClass1"))); } else if (value is SubClass1) { o.AddFirst(new JProperty("type", new JValue("SubClass2"))); } o.WriteTo(writer); } public override bool CanConvert (Type objectType) { return typeof(Base).IsAssignableFrom(objectType); } } 

Puede usar este convertidor en una clase contenedor así:

 public class Container { public List items; public string Save() { return JsonConvert.SerializeObject(items, new PolymorphicJsonConverter()) } public void Load(string jsonText) { items = JsonConvert.DeserializeObject>(jsonText, new PolymorphicJsonConverter()); } } 

Alternativamente, podría utilizar el tipo de sugerencias incorporadas de JSON.net, en lugar de hacer rodar su propio JsonConverter, pero no es tan flexible y crea JSON muy poco portátil.

 JsonConvert.SerializeObject(items, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto });