Using DataContractJsonSerializer, deserialization

2019-05-20 23:34发布

问题:

I am working on a c#.dotNet project which invokes a 3rd party REST service.

Sample Class Structure :

[Serializable]
[DataContract(Name = "MainClass")]
[KnownType(typeof(Class1))]
[KnownType(typeof(Class2))]
public class MainClass : IMainInterface
{

    public MainClass()
    {
        Value2 = new List<IClass2>();
    }

    [DataMember(Name = "class")]
    public IClass1 Value1 { get; set; }

    [DataMember(Name = "classes")]
    public List<IClass2> Value2 { get; set; }

}

[Serializable]
[Export(typeof(IClass1))]
[ExportMetadata("IClass1", "Class1")]
[DataContract(Name = "class1")]
public class Class1 : IClass1
{
    [ImportingConstructor]
    public Class1()
    {

    }

    [DataMember(Name = "prop1")]
    public string Prop1 { get; set; }

    [DataMember(Name = "prop2")]
    public string Prop2 { get; set; }

    [DataMember(Name = "prop3")]
    public string Prop3 { get; set; }

}

[Serializable]
[Export(typeof(IClass2))]
[ExportMetadata("IClass2", "Class2")]
[DataContract]
public class Class2 : IClass2
{
    [ImportingConstructor]
    public Class2()
    { }


    [DataMember(Name = "propA")]
    public string PropA { get; set; }

    [DataMember(Name = "propB")]
    public string PropB { get; set; }

    [DataMember(Name = "propC")]
    public string PropC { get; set; }
}

public interface IMainInterface
{
    IClass1 Value1 { get; set; }

    List<IClass2> Value2 { get; set; }
}

public interface IClass1
{
    string Prop1 { get; set; }

    string Prop2 { get; set; }

    string Prop3 { get; set; }
}

public interface IClass2
{
    string PropA { get; set; }

    string PropB { get; set; }

    string PropC { get; set; }
}

Json_String1: (with type hint)

{
"class":
    {"__type":"class1:#WpfApplication1","prop1":"TestVal0","prop2":"TestVal2","prop3":"TestVal3"},
"classes":
    [
        {"__type":"Class2:#WpfApplication1","propA":"A","propB":"B","propC":"C"},
        {"__type":"Class2:#WpfApplication1","propA":"X","propB":"Y","propC":"Z"},
        {"__type":"Class2:#WpfApplication1","propA":"1","propB":"2","propC":"3"}
    ]
}

Json_String2: (without type hint)

{
"class":
    {"prop1":"TestVal0","prop2":"TestVal2","prop3":"TestVal3"},
"classes":
    [
        {"propA":"A","propB":"B","propC":"C"},
        {"propA":"X","propB":"Y","propC":"Z"},
        {"propA":"1","propB":"2","propC":"3"}
    ]
}

So, for given class structure if I generate json (of object of MainClass) using DataContractJsonSerializer, I am getting Json_String1 and if i directly deserialize, it works fine.

Whereas as while GETting data, response is Json_String2 ( w/o type hint). Hence, while deserializing I get following error.

InvalidCastException was unhandled. Unable to cast object of type 'System.Object' to type 'WpfApplication1.IClass2'.

Now, I manually have to modify above json (string manipulation) by adding type hint, to deserialize it successfully.

Question 1) how can I avoid this Json String Manipulation for deserializing ?

Question 2) how can I create json without type hint ?

edit : 1. Added IMainInterface which is implemented by MainClass. 2. dotNet Framework 4

回答1:

Since none of your classes are actually polymorphic, there are a couple solutions available that use .Net built-in class libraries:

Solution 1: JavaScriptSerializer Solution

JavaScriptSerializer makes it easy to remap interfaces to classes during deserialization by using a JavaScriptConverter. However, it does not allow field and property names to be remapped, thus your property names must match the names in the JSON you wish to process. The following converter does the trick:

public class InterfaceToClassConverter<TInterface, TClass> : JavaScriptConverter where TClass : class, TInterface
{
    public InterfaceToClassConverter()
    {
        if (typeof(TInterface) == typeof(TClass))
            throw new ArgumentException(string.Format("{0} and {1} must not be the same type", typeof(TInterface).FullName, typeof(TClass).FullName)); // Or else you would get infinite recursion!
        if (!typeof(TInterface).IsInterface)
            throw new ArgumentException(string.Format("{0} must be an interface", typeof(TInterface).FullName));
        if (typeof(TClass).IsInterface)
            throw new ArgumentException(string.Format("{0} must be a class not an interface", typeof(TClass).FullName));
    }

    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
        if (type == typeof(TInterface))
            return serializer.ConvertToType<TClass>(dictionary);
        return null;
    }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override IEnumerable<Type> SupportedTypes
    {
        get
        {
            // For an interface-valued property such as "IFoo  Foo { getl set; },
            // When serializing, JavaScriptSerializer knows the actual concrete type being serialized -- which is never an interface.
            // When deserializing, JavaScriptSerializer only knows the expected type, which is an interface.  Thus by returning
            // only typeof(TInterface), we ensure this converter will only be called during deserialization, not serialization.
            return new[] { typeof(TInterface) };
        }
    }
}

And use it like:

public interface IMainInterface
{
    IClass1 @class { get; set; } // NOTICE ALL PROPERTIES WERE RENAMED TO MATCH THE JSON NAMES.
    List<IClass2> classes { get; set; }
}

[Serializable]
[DataContract(Name = "MainClass")]
[KnownType(typeof(Class1))]
[KnownType(typeof(Class2))]
public class MainClass : IMainInterface
{
    public MainClass()
    {
        classes = new List<IClass2>();
    }

    [DataMember(Name = "class")]
    public IClass1 @class { get; set; }

    [DataMember(Name = "classes")]
    public List<IClass2> classes { get; set; }
}

[Serializable]
[DataContract(Name = "class1")]
public class Class1 : IClass1
{
    public Class1() {}

    [DataMember(Name = "prop1")]
    public string prop1 { get; set; }

    [DataMember(Name = "prop2")]
    public string prop2 { get; set; }

    [DataMember(Name = "prop3")]
    public string prop3 { get; set; }

}

[Serializable]
[DataContract]
public class Class2 : IClass2
{
    public Class2() { }

    [DataMember(Name = "propA")]
    public string propA { get; set; }

    [DataMember(Name = "propB")]
    public string propB { get; set; }

    [DataMember(Name = "propC")]
    public string propC { get; set; }
}

public interface IClass1
{
    string prop1 { get; set; }
    string prop2 { get; set; }
    string prop3 { get; set; }
}

public interface IClass2
{
    string propA { get; set; }
    string propB { get; set; }
    string propC { get; set; }
}

public static class TestJavaScriptConverter
{
    public static void Test()
    {
        string json = @"
            {
            ""class"":
                {""prop1"":""TestVal0"",""prop2"":""TestVal2"",""prop3"":""TestVal3""},
            ""classes"":
                [
                    {""propA"":""A"",""propB"":""B"",""propC"":""C""},
                    {""propA"":""X"",""propB"":""Y"",""propC"":""Z""},
                    {""propA"":""1"",""propB"":""2"",""propC"":""3""}
                ]
            }";

        var serializer = new JavaScriptSerializer();
        serializer.RegisterConverters(new JavaScriptConverter[] { new InterfaceToClassConverter<IClass1, Class1>(), new InterfaceToClassConverter<IClass2, Class2>() });
        var main1 = serializer.Deserialize<MainClass>(json);
        var json2 = serializer.Serialize(main1);
        Debug.WriteLine(json2);
        var main2 = serializer.Deserialize<MainClass>(json2);

        Debug.Assert(main1.@class.ToStringWithReflection() == main2.@class.ToStringWithReflection()); // No assert
        Debug.Assert(main1.classes.Select(c => c.ToStringWithReflection()).SequenceEqual(main2.classes.Select(c => c.ToStringWithReflection()))); // no assert
    }
}

Solution 2: DataContractJsonSerializer Solution

WCF and its DataContractSerializers work only with concrete types and do not serialize interfaces. Thus, if you wish to use these serializers you must use concrete classes internally and present them to the outside world as interfaces, for instance:

public interface IMainInterface
{
    IClass1 Value1 { get; set; }
    IList<IClass2> Value2 { get; set; }
}

[Serializable]
[DataContract(Name = "MainClass")]
public class MainClass : IMainInterface
{
    [DataMember(Name = "class")]
    Class1 RealValue1 { get; set; }

    [DataMember(Name = "classes")]
    private List<Class2> RealList2 { get; set; }

    IList<IClass2> list2Proxy; // can't be readonly because the DataContactJsonSerializer does not call the default constructor.

    private IList<IClass2> List2Proxy
    {
        get
        {
            if (list2Proxy == null)
                Interlocked.CompareExchange(ref list2Proxy, new ConvertingList<Class2, IClass2>(() => this.RealList2, c => c, ToClass), null);
            return list2Proxy;
        }
    }

    Class2 ToClass(IClass2 iClass)
    {
        // REWRITE TO FIT YOUR NEEDS
        return (Class2)iClass;
    }

    Class1 ToClass(IClass1 iClass)
    {
        // REWRITE TO FIT YOUR NEEDS
        return (Class1)iClass;
    }

    public MainClass()
    {
        RealList2 = new List<Class2>();
    }

    [IgnoreDataMember]
    public IClass1 Value1
    {
        get
        {
            return RealValue1;
        }
        set
        {
            RealValue1 = ToClass(value);
        }
    }

    [IgnoreDataMember]
    public IList<IClass2> Value2
    {
        get
        {
            return List2Proxy;
        }
        set
        {
            if (value == null)
            {
                RealList2.Clear();
                return;
            }
            if (List2Proxy == value)
                return;
            RealList2 = value.Select<IClass2, Class2>(ToClass).ToList();
        }
    }
}

public class ConvertingList<TIn, TOut> : IList<TOut>
{
    readonly Func<IList<TIn>> getList;
    readonly Func<TIn, TOut> toOuter;
    readonly Func<TOut, TIn> toInner;

    public ConvertingList(Func<IList<TIn>> getList, Func<TIn, TOut> toOuter, Func<TOut, TIn> toInner)
    {
        if (getList == null || toOuter == null || toInner == null)
            throw new ArgumentNullException();
        this.getList = getList;
        this.toOuter = toOuter;
        this.toInner = toInner;
    }

    IList<TIn> List { get { return getList(); } }

    TIn ToInner(TOut outer) { return toInner(outer); }

    TOut ToOuter(TIn inner) { return toOuter(inner); }

    #region IList<TOut> Members

    public int IndexOf(TOut item)
    {
        return List.IndexOf(toInner(item));
    }

    public void Insert(int index, TOut item)
    {
        List.Insert(index, ToInner(item));
    }

    public void RemoveAt(int index)
    {
        List.RemoveAt(index);
    }

    public TOut this[int index]
    {
        get
        {
            return ToOuter(List[index]);
        }
        set
        {
            List[index] = ToInner(value);
        }
    }

    #endregion

    #region ICollection<TOut> Members

    public void Add(TOut item)
    {
        List.Add(ToInner(item));
    }

    public void Clear()
    {
        List.Clear();
    }

    public bool Contains(TOut item)
    {
        return List.Contains(ToInner(item));
    }

    public void CopyTo(TOut[] array, int arrayIndex)
    {
        foreach (var item in this)
            array[arrayIndex++] = item;
    }

    public int Count
    {
        get { return List.Count; }
    }

    public bool IsReadOnly
    {
        get { return List.IsReadOnly; }
    }

    public bool Remove(TOut item)
    {
        return List.Remove(ToInner(item));
    }

    #endregion

    #region IEnumerable<TOut> Members

    public IEnumerator<TOut> GetEnumerator()
    {
        foreach (var item in List)
            yield return ToOuter(item);
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion
}

Note that if a caller attempts to set an IClass1 or IClass2 which is not an actual Class1 or Class2, an InvalidCastException will be thrown. Thus this gives the appearance of hiding interface implementations without truly keeping the implementations private.



回答2:

The issue is because you're trying to deserialize to a class that only has interfaces. It obviously works when the JSON specifies the __type. But when it doesn't (like in your second JSON example), ReadObject isn't able to automatically resolve the interfaces to their implementation.

Try using the concrete classes Class1, Class2 in MainClass instead of their interfaces (IClass1, IClass2). The rest of the code can remain as-is. Both your Json examples should work fine with this.

[Serializable]
[DataContract(Name = "MainClass")]
[KnownType(typeof(Class1))]
[KnownType(typeof(Class2))]
public class MainClass
{

    public MainClass()
    {
        Value2 = new List<Class2>();
    }

    [DataMember(Name = "class")]
    public Class1 Value1 { get; set; }

    [DataMember(Name = "classes")]
    public List<Class2> Value2 { get; set; }

}