Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserializing JSON to abstract class

I am trying to deserialize a JSON string to a concrete class, which inherits from an abstract class, but I just can't get it working. I have googled and tried some solutions but they don't seem to work either.

This is what I have now:

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

However, if I try to cast the resulting object, it just doesn't work.

The reason why I don't use DeserializeObject is that I have many concrete classes.

Any suggestions?

  • I am using Newtonsoft.Json
like image 430
aochagavia Avatar asked Jan 08 '14 12:01

aochagavia


4 Answers

One may not want to use TypeNameHandling (because one wants more compact json or wants to use a specific name for the type variable other than "$type"). Meanwhile, the customCreationConverter approach will not work if one wants to deserialize the base class into any of multiple derived classes without knowing which one to use in advance.

An alternative is to use an int or other type in the base class and define a 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; }
}

The JsonConverter for the base class can then deserialize the object based on its type. The complication is that to avoid a stack overflow (where the JsonConverter repeatedly calls itself), a custom contract resolver must be used during this deserialization.

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

That's it. Now you can use serialize/deserialize any derived class. You can also use the base class in other classes and serialize/deserialize those without any additional work:

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);
like image 136
mbabramo Avatar answered Nov 16 '22 02:11

mbabramo


I would suggest to use CustomCreationConverter in the following way:

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();
            }
        }
    }
like image 27
Denis Avatar answered Nov 16 '22 04:11

Denis


try something like this

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

UPDATE
for this scenario methinks all work as you want

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

where c type is test.Base {test.Der}

UPDATE

@Gusman suggest use TypeNameHandling.Objects instead of TypeNameHandling.All. It is enough and it will produce a less verbose serialization.

like image 23
Grundy Avatar answered Nov 16 '22 04:11

Grundy


Actually, as it has been stated in an update, the simplest way (in 2019) is to use a simple custom pre-defined JsonSerializerSettings, as explained here

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

And for deserializing :

TDSPriceModels models = JsonConvert.DeserializeObject<TDSPriceModels>(File.ReadAllText(jsonPath), new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.All
        });
like image 22
XavierAM Avatar answered Nov 16 '22 03:11

XavierAM