Используя DataContractJsonSerializer, десериализацию строки JSON в объект C#, который имеет список и интерфейс в качестве свойств

Я работаю над проектом aC#.dotNet, который вызывает сторонний REST-сервис.

Типовая структура класса:

[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: (с подсказкой типа)

{
"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: (без подсказки типа)

{
"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"}
    ]
}

Таким образом, для данной структуры класса, если я генерирую JSON (объекта MainClass) с помощью DataContractJsonSerializer Я получаю Json_String1, и если я непосредственно десериализовать, он работает нормально.

В то время как при получении данных, ответом является Json_String2 (без подсказки типа). Следовательно, при десериализации я получаю следующую ошибку.

Invalid Cast Exception

InvalidCastException не было обработано. Невозможно привести объект типа "System.Object" к типу "WpfApplication1.IClass2".

Теперь мне нужно вручную изменить вышеуказанный json (манипулирование строками), добавив подсказку типа, чтобы успешно десериализовать его.

Вопрос 1) как я могу избежать этой Json String Manipulation для десериализации?

Вопрос 2) как я могу создать JSON без подсказки типа?

edit: 1. Добавлен IMainInterface, который реализован в MainClass. 2. dotNet Framework 4

2 ответа

Решение

Поскольку ни один из ваших классов на самом деле не является полиморфным, существует несколько доступных решений, использующих встроенные библиотеки классов.Net:

Решение 1: JavaScriptSerializerРешение

JavaScriptSerializer облегчает переназначение интерфейсов к классам во время десериализации с помощью JavaScriptConverter, Однакоон не позволяет переназначать имена полей и свойств, поэтому имена ваших свойств должны совпадать с именами в JSON, который вы хотите обработать. Следующий конвертер делает свое дело:

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

И используйте это как:

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

Решение 2:DataContractJsonSerializerРешение

WCF и его DataContractSerializerРаботают только с конкретными типами и не сериализуют интерфейсы. Таким образом, если вы хотите использовать эти сериализаторы, вы должны использовать конкретные классы внутри и представлять их внешнему миру как интерфейсы, например:

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
}

Обратите внимание, что если вызывающий пытается установить IClass1 или же IClass2 что не актуально Class1 или же Class2, InvalidCastException будет брошен. Таким образом, это создает видимость сокрытия реализаций интерфейса, не сохраняя при этом приватность реализаций.

Проблема в том, что вы пытаетесь десериализовать класс, который имеет только интерфейсы. Очевидно, это работает, когда JSON определяет __type. Но когда этого не происходит (как во втором примере JSON), ReadObject не может автоматически разрешить интерфейсы для их реализации.

Попробуйте использовать конкретные классы Class1, Class2 в MainClass вместо их интерфейсов (IClass1, IClass2). Остальная часть кода может оставаться как есть. Оба ваших примера Json должны хорошо с этим работать.

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

}
Другие вопросы по тегам