web-dev-qa-db-fra.com

Désérialisation de JSON en classe abstraite

J'essaie de désérialiser une chaîne JSON en une classe concrète, qui hérite d'une classe abstraite, mais je n'arrive pas à la faire fonctionner. J'ai googlé et essayé quelques solutions mais elles ne semblent pas fonctionner non plus.

Voici ce que j'ai maintenant:

abstract class AbstractClass { }

class ConcreteClass { }

public AbstractClass Decode(string jsonString)
{
    JsonSerializerSettings jss = new JsonSerializerSettings();
    jss.TypeNameHandling = TypeNameHandling.All;
    return (AbstractClass)JsonConvert.DeserializeObject(jsonString, null, jss);
}

Cependant, si j'essaie de lancer l'objet résultant, cela ne fonctionne tout simplement pas.

La raison pour laquelle je n'utilise pas DeserializeObject est que j'ai beaucoup de classes concrètes.

Aucune suggestion?

  • J'utilise Newtonsoft.Json
32
aochagavia

On peut ne pas vouloir utiliser TypeNameHandling (car on veut un json plus compact ou on veut utiliser un nom spécifique pour la variable de type autre que "$ type"). Pendant ce temps, approche customerCreationConverter ne fonctionnera pas si l'on veut désérialiser la classe de base en l'une des multiples classes dérivées sans savoir laquelle utiliser à l'avance.

Une alternative consiste à utiliser un type int ou autre dans la classe de base et à définir un JsonConverter.

[JsonConverter(typeof(BaseConverter))]
abstract class Base
{
    public int ObjType { get; set; }
    public int Id { get; set; }
}

class DerivedType1 : Base
{
    public string Foo { get; set; }
}

class DerivedType2 : Base
{
    public string Bar { get; set; }
}

Le JsonConverter pour la classe de base peut ensuite désérialiser l'objet en fonction de son type. La complication est que pour éviter un débordement de pile (où le JsonConverter s'appelle à plusieurs reprises), un résolveur de contrat personnalisé doit être utilisé pendant cette désérialisation.

public class BaseSpecifiedConcreteClassConverter : DefaultContractResolver
{
    protected override JsonConverter ResolveContractConverter(Type objectType)
    {
        if (typeof(Base).IsAssignableFrom(objectType) && !objectType.IsAbstract)
            return null; // pretend TableSortRuleConvert is not specified (thus avoiding a stack overflow)
        return base.ResolveContractConverter(objectType);
    }
}

public class BaseConverter : JsonConverter
{
    static JsonSerializerSettings SpecifiedSubclassConversion = new JsonSerializerSettings() { ContractResolver = new BaseSpecifiedConcreteClassConverter() };

    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Base));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        switch (jo["ObjType"].Value<int>())
        {
            case 1:
                return JsonConvert.DeserializeObject<DerivedType1>(jo.ToString(), SpecifiedSubclassConversion);
            case 2:
                return JsonConvert.DeserializeObject<DerivedType2>(jo.ToString(), SpecifiedSubclassConversion);
            default:
                throw new Exception();
        }
        throw new NotImplementedException();
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException(); // won't be called because CanWrite returns false
    }
}

C'est ça. Vous pouvez maintenant utiliser sérialiser/désérialiser n'importe quelle classe dérivée. Vous pouvez également utiliser la classe de base dans d'autres classes et sérialiser/désérialiser celles sans aucun travail supplémentaire:

class Holder
    {
        public List<Base> Objects { get; set; }
    }
string json = @"
        [
            {
                ""Objects"" : 
                [
                    { ""ObjType"": 1, ""Id"" : 1, ""Foo"" : ""One"" },
                    { ""ObjType"": 1, ""Id"" : 2, ""Foo"" : ""Two"" },
                ]
            },
            {
                ""Objects"" : 
                [
                    { ""ObjType"": 2, ""Id"" : 3, ""Bar"" : ""Three"" },
                    { ""ObjType"": 2, ""Id"" : 4, ""Bar"" : ""Four"" },
                ]
            },
        ]";

            List<Holder> list = JsonConvert.DeserializeObject<List<Holder>>(json);
            string serializedAgain = JsonConvert.SerializeObject(list);
            Debug.WriteLine(serializedAgain);
52
mbabramo

essayez quelque chose comme ça

public AbstractClass Decode(string jsonString)
{
    var jss = new JavaScriptSerializer();
    return jss.Deserialize<ConcreteClass>(jsonString);
}

[~ # ~] mise à jour [~ # ~]
pour ce scénario, je pense que tout fonctionne comme vous le souhaitez

public abstract class Base
{
    public abstract int GetInt();
}
public class Der:Base
{
    int g = 5;
    public override int GetInt()
    {
        return g+2;
    }
}
public class Der2 : Base
{
    int i = 10;
    public override int GetInt()
    {
        return i+17;
    }
}

....

var jset = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All };
Base b = new Der()
string json = JsonConvert.SerializeObject(b, jset);
....

Base c = (Base)JsonConvert.DeserializeObject(json, jset);

c type est test.Base {test.Der}

[~ # ~] mise à jour [~ # ~]

@ Gusman suggère d'utiliser TypeNameHandling.Objects au lieu de TypeNameHandling.All. C'est suffisant et cela produira une sérialisation moins verbeuse.

21
Grundy

Je suggérerais d'utiliser CustomCreationConverter de la manière suivante:

public enum ClassDiscriminatorEnum
    {
        ChildClass1,
        ChildClass2
    }

    public abstract class BaseClass
    {
        public abstract ClassDiscriminatorEnum Type { get; }
    }

    public class Child1 : BaseClass
    {
        public override ClassDiscriminatorEnum Type => ClassDiscriminatorEnum.ChildClass1;
        public int ExtraProperty1 { get; set; }
    }

    public class Child2 : BaseClass
    {
        public override ClassDiscriminatorEnum Type => ClassDiscriminatorEnum.ChildClass2;
    }

    public class BaseClassConverter : CustomCreationConverter<BaseClass>
    {
        private ClassDiscriminatorEnum _currentObjectType;

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var jobj = JObject.ReadFrom(reader);
            _currentObjectType = jobj["Type"].ToObject<ClassDiscriminatorEnum>();
            return base.ReadJson(jobj.CreateReader(), objectType, existingValue, serializer);
        }

        public override BaseClass Create(Type objectType)
        {
            switch (_currentObjectType)
            {
                case ClassDiscriminatorEnum.ChildClass1:
                    return new Child1();
                case ClassDiscriminatorEnum.ChildClass2:
                    return new Child2();
                default:
                    throw new NotImplementedException();
            }
        }
    }
8
Denis
 public class CustomConverter : JsonConverter
{
    private static readonly JsonSerializer Serializer = new JsonSerializer();

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jObject = JObject.Load(reader);
        var typeString = jObject.Value<string>("Kind"); //Kind is a property in json , from which we know type of child classes
        var requiredType = RecoverType(typeString);

        return Serializer.Deserialize(jObject.CreateReader(), requiredType);
    }

    private Type RecoverType(string typeString)
    {
        if (typeString.Equals(type of child class1, StringComparison.OrdinalIgnoreCase))
            return typeof(childclass1);
        if (typeString.Equals(type of child class2, StringComparison.OrdinalIgnoreCase))
            return typeof(childclass2);            

        throw new ArgumentException("Unrecognized type");
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Base class).IsAssignableFrom(objectType) || typeof((Base class) == objectType;
    }

    public override bool CanWrite { get { return false; } }
}

Maintenant, ajoutez ce convertisseur dans JsonSerializerSettings comme ci-dessous

   var jsonSerializerSettings = new JsonSerializerSettings();
        jsonSerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
        jsonSerializerSettings.Converters.Add(new CustomConverter());

Après avoir ajouté un objet de classe de base sérialiser ou désérialiser comme ci-dessous

 JsonConvert.DeserializeObject<Type>("json string", jsonSerializerSettings );
3
Dalip Choudhary

En fait, comme cela a été indiqué dans une mise à jour, la manière la plus simple (en 2019) est d'utiliser un simple JsonSerializerSettings personnalisé prédéfini, comme expliqué ici

        string jsonTypeNameAll = JsonConvert.SerializeObject(priceModels, Formatting.Indented,new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });

Et pour désérialiser:

TDSPriceModels models = JsonConvert.DeserializeObject<TDSPriceModels>(File.ReadAllText(jsonPath), new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });
2
XavierAM