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

Resuelto GaTechThomas asked hace 9 años • 3 respuestas

Nos gustaría poder serializar/deserializar json desde/hacia clases de C#, donde la clase principal tenga 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 "$tipo".

La primera idea es poder cambiar el nombre de "$tipo" a un valor de nuestra elección y hacer que el valor del tipo sea una enumeración que asigne los subtipos correctamente. No lo he visto como una opción, pero me encantaría saber si es posible.

El segundo pensamiento fue el siguiente: A continuación se muestra un primer paso por las clases, donde la clase de nivel superior tiene un indicador (SubTypeType) sobre qué tipo de datos está contenido en el objeto secundario (SubTypeData). Busqué un poco en la documentación de Json.Net y probé algunas cosas, pero no tuve suerte.

Actualmente tenemos control total sobre la definición de datos, pero una vez que se implementa, todo queda bloqueado.

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; }
}
GaTechThomas avatar Apr 09 '15 09:04 GaTechThomas
Aceptado

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

  1. No se puede acceder a la instancia de la clase contenedora cuando Json.NET lee la clase contenida.
  2. Si luego necesita convertir la SubTypeClassBasepropiedad en, digamos, una lista, no habrá ningún lugar donde colocar la información del subtipo.

En su lugar, 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 la enumeración de subtipo personalizado se serializará cada vez que SubTypeClassBasese serialice un objeto asignable. Una vez hecho esto, para la deserialización, puede crear un archivo JsonConverterque cargue el json de un determinado SubTypeClassBaseen un archivo temporal JObject, verifique el valor de la "Type"propiedad 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<Type, SubType> typeToSubType;
    static readonly Dictionary<SubType, Type> subTypeToType;

    static SubTypeClassBase()
    {
        typeToSubType = new Dictionary<Type,SubType>()
        {
            { 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<SubType>(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();
    }
}
dbc avatar Apr 09 '2015 06:04 dbc

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

Suponiendo que tenemos la siguiente estructura de clases:

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 JSON denominado typeal serializar y lo lee al deserializar.

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<string>();

        if (type == "SubClass1") {
            return item.ToObject<SubClass1>();
        } else if (type == "SubClass2") {
            return item.ToObject<SubClass2>();
        } 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);
    }
}

Podrías usar este convertidor en una clase contenedora como esta:

public class Container {
    public List<Base> items;

    public string Save() {
        return JsonConvert.SerializeObject(items, new PolymorphicJsonConverter())
    }

    public void Load(string jsonText) {
        items = JsonConvert.DeserializeObject<List<Base>>(jsonText, new PolymorphicJsonConverter());
    }
}

Alternativamente, puede usar las sugerencias de tipo integradas de JSON.net, en lugar de implementar su propio JsonConverter, pero no es tan flexible y crea un JSON muy poco portátil.

JsonConvert.SerializeObject(items, new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.Auto
});
blade avatar Mar 27 '2018 16:03 blade

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

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<MainClass>("{\"SubTypeData\":{\"ZzzField\":\"zzz\",\"SubTypeType\":1}}");
            Assert.AreEqual("zzz", (obj.SubTypeData as SubTypeClass2)?.ZzzField);
        }
manuc66 avatar Jun 20 '2017 18:06 manuc66