使用JSON.Net在Dictionary中复杂类型的特定于用法的序列化

Pha*_*o2k 3 c# serialization json json.net complextype

我上课了

public class MyValue 
{
    public String Prop1 { get; set; }
    public String Prop2 { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我用它作为普通属性的类型以及字典键.

我需要的是这样一种方式,当这个类被用作属性时,它被序列化为

{"Prop1":"foo","Prop2":"bar"}
Run Code Online (Sandbox Code Playgroud)

但是当它用作Dictionary键时,它的序列化方式是JSON.Net能够正确地反序列化它.

在向MyValue添加ToString()方法时,我能够创建一个允许用作Dictionary键的文本表示(非JSON),但不幸的是,我之后无法反序列化它.即使为MyValue添加JsonConverter也无济于事,因为它似乎无法将非JSON作为源格式处理(另外,当序列化为属性时,它是IS json,因此转换器需要以某种方式处理它们).

dbc*_*dbc 6

您可以做的是在代理KeyValuePair<string, string>数组中序列化和反序列化您的字典,如下所示:

[DataContract]
public class MyContainer
{
    public MyContainer() {
        this.Dictionary = new Dictionary<MyValue, int>();
    }

    [DataMember]
    public MyValue MyValue { get; set; }

    [IgnoreDataMember]
    public Dictionary<MyValue, int> Dictionary { get; set; }

    [DataMember(Name="Dictionary")]
    private KeyValuePair<MyValue, int> [] SerializedDictionary
    {
        get
        {
            if (Dictionary == null)
                return null;
            return Dictionary.ToArray();
        }
        set
        {
            if (value == null)
            {
                Dictionary = null;
            }
            else
            {
                Dictionary = value.ToDictionary(pair => pair.Key, pair => pair.Value);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

(这里我使用的是DataContract属性,但我可以很容易地使用[JsonIgnore][JsonProperty("Dictionary")])

因此,为了测试这个(并假设你已经正确地重写GetHashCode()Equals()打开MyValue,你需要做它以便将它用作字典键),我做了以下事情:

public static class TestDictionaryJson
{
    public static void Test()
    {
        var dict = new Dictionary<MyValue, int>();
        dict[(new MyValue("A", "A"))] = 1;
        dict[(new MyValue("B", "B"))] = 2;

        var myContainer = new MyContainer() { MyValue = new MyValue("A Property", "At the top level"), Dictionary = dict };

        var json = JsonConvert.SerializeObject(myContainer, Formatting.Indented);

        Debug.WriteLine(json);

        try
        {
            var newContainer = JsonConvert.DeserializeObject<MyContainer>(json);
        }
        catch (Exception ex)
        {
            Debug.Assert(false, ex.ToString()); // No assert - no exception is thrown.
        }

        try
        {
            var dictjson = JsonConvert.SerializeObject(dict, Formatting.Indented);
            Debug.WriteLine(dictjson);
            var newDict = JsonConvert.DeserializeObject<Dictionary<MyValue, int>>(dictjson);
        }
        catch (Exception ex)
        {
            Debug.WriteLine("Caught expected exception deserializing dictionary directly: " + ex.ToString());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

果然,对容器进行反序列化没有例外,但是直接对字典进行了反序列化.并为容器创建了以下JSON:

{
  "MyValue": {
    "Prop1": "A Property",
    "Prop2": "At the top level"
  },
  "Dictionary": [
    {
      "Key": {
        "Prop1": "A",
        "Prop2": "A"
      },
      "Value": 1
    },
    {
      "Key": {
        "Prop1": "B",
        "Prop2": "B"
      },
      "Value": 2
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

那是你要的吗?

更新

或者,如果您不喜欢代理数组,可以将以下内容JsonConverterAttribute应用于每个Dictionary属性以获得相同的结果:

public class MyContainer
{
    public MyContainer()
    {
        this.Dictionary = new Dictionary<MyValue, int>();
    }

    public MyValue MyValue { get; set; }

    [JsonConverter(typeof(DictionaryToArrayConverter<MyValue, int>))]
    public Dictionary<MyValue, int> Dictionary { get; set; }
}

public class DictionaryToArrayConverter<TKey, TValue> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Dictionary<TKey, TValue>);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        KeyValuePair<TKey, TValue>[] pairs;

        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Array)
        {
            pairs = token.ToObject<KeyValuePair<TKey, TValue>[]>(serializer);
        }
        else
        {
            JArray array = new JArray();
            array.Add(token);
            pairs = token.ToObject<KeyValuePair<TKey, TValue>[]>(serializer);
        }
        if (pairs == null)
            return null;
        return pairs.ToDictionary(pair => pair.Key, pair => pair.Value);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null)
            return;
        var pairs = ((IDictionary<TKey, TValue>)value).ToArray();
        serializer.Serialize(writer, pairs);
    }
}
Run Code Online (Sandbox Code Playgroud)

更新

作为替代方案,您可以密封您的MyValue班级并附加适合TypeConverterAttribute于从&转换为字符串.JSON.Net会选择它并将其用于字典键和属性.这个解决方案更简单,因为它是一个全局解决方案,因此您不需要为每个字典使用代理数组或转换器属性,但是为您的MyValue属性创建的JSON 并不是您所需要的.

从而:

public class MyValueConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context,
       Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }
        return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context,
       CultureInfo culture, object value)
    {
        if (value is string)
        {
            // Cannot do JsonConvert.DeserializeObject here because it will cause a stackoverflow exception.
            using (var reader = new JsonTextReader(new StringReader((string)value)))
            {
                JObject item = JObject.Load(reader);
                if (item == null)
                    return null;
                MyValue myValue = new MyValue();
                var prop1 = item["Prop1"];
                if (prop1 != null)
                    myValue.Prop1 = prop1.ToString();
                var prop2 = item["Prop2"];
                if (prop2 != null)
                    myValue.Prop2 = prop2.ToString();
                return myValue;
            }
        }
        return base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context,
       CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == typeof(string))
        {
            MyValue myValue = (MyValue)value;

            // Cannot do JsonConvert.SerializeObject here because it will cause a stackoverflow exception.
            StringBuilder sb = new StringBuilder();
            using (StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture))
            using (JsonTextWriter jsonWriter = new JsonTextWriter(sw))
            {
                jsonWriter.WriteStartObject();
                jsonWriter.WritePropertyName("Prop1");
                jsonWriter.WriteValue(myValue.Prop1);
                jsonWriter.WritePropertyName("Prop2");
                jsonWriter.WriteValue(myValue.Prop2);
                jsonWriter.WriteEndObject();

                return sw.ToString();
            }
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}

[TypeConverter(typeof(MyValueConverter))]
public class MyValue
{
    public MyValue()
    {
    }

    public MyValue(string prop1, string prop2)
    {
        this.Prop1 = prop1;
        this.Prop2 = prop2;
    }

    public String Prop1 { get; set; }
    public String Prop2 { get; set; }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj))
            return true;
        else if (ReferenceEquals(obj, null))
            return false;
        if (GetType() != obj.GetType())
            return false;
        var other = (MyValue)obj;
        return Prop1 == other.Prop1 && Prop2 == other.Prop2;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            uint code = 0;
            if (Prop1 != null)
                code ^= (uint)Prop1.GetHashCode();
            code = (code << 16) | (code >> 16);
            if (Prop2 != null)
                code ^= (uint)Prop2.GetHashCode();
            return (int)code;
        }
    }

    public override string ToString()
    {
        return TypeDescriptor.GetConverter(GetType()).ConvertToString(this);
    }

    public static bool operator ==(MyValue first, MyValue second)
    {
        if (ReferenceEquals(first, null))
            return ReferenceEquals(second, null);
        return first.Equals(second);
    }

    public static bool operator !=(MyValue first, MyValue second)
    {
        return !(first == second);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在可以在不使用任何代理数组的情况下序列化使用此类的属性和字典.例如,序列化和反序列化以下内容:

public class MyContainer
{
    public MyContainer()
    {
        this.Dictionary = new Dictionary<MyValue, int>();
    }

    public MyValue MyValue { get; set; }

    public Dictionary<MyValue, int> Dictionary { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

序列化时给出以下JSON:

{
  "MyValue": "{\"Prop1\":\"A Property\",\"Prop2\":\"At the top level\"}",
  "Dictionary": {
    "{\"Prop1\":\"A\",\"Prop2\":\"A\"}": 1,
    "{\"Prop1\":\"B\",\"Prop2\":\"B\"}": 2
  }
}
Run Code Online (Sandbox Code Playgroud)

(引号被转义,因为它们嵌入在JSON中,而不是JSON的一部分.)

延迟更新 - TypeConverter为字典键创建通用

TypeConverter通过使用适当的合同解析器,可以创建适用于任何通用指定类型的通用:

public class NoTypeConverterContractResolver : DefaultContractResolver
{
    readonly Type type;

    public NoTypeConverterContractResolver(Type type)
        : base()
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type == typeof(string) || type.IsPrimitive)
            throw new ArgumentException("type == typeof(string) || type.IsPrimitive");
        this.type = type;
    }

    protected override JsonContract CreateContract(Type objectType)
    {
        if (type.IsAssignableFrom(objectType))
        {
            // Replaces JsonStringContract for the specified type.
            var contract = this.CreateObjectContract(objectType);
            return contract;
        }
        return base.CreateContract(objectType);
    }
}

public class GenericJsonTypeConverter<T> : TypeConverter
{
    // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
    // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
    // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
    // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
    static NoTypeConverterContractResolver contractResolver;

    static NoTypeConverterContractResolver ContractResolver
    {
        get
        {
            if (contractResolver == null)
                Interlocked.CompareExchange(ref contractResolver, new NoTypeConverterContractResolver(typeof(T)), null);
            return contractResolver;
        }
    }

    public override bool CanConvertFrom(ITypeDescriptorContext context,
       Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }
        return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context,
       CultureInfo culture, object value)
    {
        if (value is string)
        {
            using (var reader = new JsonTextReader(new StringReader((string)value)))
            {
                var obj = JsonSerializer.Create(new JsonSerializerSettings { ContractResolver = ContractResolver }).Deserialize<T>(reader);
                return obj;
            }
        }
        return base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == typeof(string))
        {
            StringBuilder sb = new StringBuilder();
            using (StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture))
            using (JsonTextWriter jsonWriter = new JsonTextWriter(sw))
            {
                JsonSerializer.Create(new JsonSerializerSettings { ContractResolver = ContractResolver }).Serialize(jsonWriter, value);
            }
            return sb.ToString();
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}
Run Code Online (Sandbox Code Playgroud)

然后将其应用于您的课程,如下所示:

[TypeConverter(typeof(GenericJsonTypeConverter<MyValue>))]
public class MyValue
{

}
Run Code Online (Sandbox Code Playgroud)