JsonConverter,其属性由其父

时间:2016-08-31 13:34:05

标签: c# serialization json.net

我遇到一种情况,即API有多个类似于数组的对象作为对象的单个属性。例如:

"parent": {
    id: 4,
    /*... other fields ...*/
    "prop_1": "A",
    "prop_2": "B",
    /*... other "props" ...*/
    "prop_24": "W"
}

我希望C#中的结果模型不重复相同的结构,并将prop_X反序列化为List并序列化回到那个混乱。

class Parent {
    [JsonProperty("id")]
    public int ParentId { get; set; }
    /*... other properties ...*/
    public List<string> Props { get; set; }
}

我尝试将JsonConverter属性添加到Props属性,但我无法弄清楚如何在父级上获取所需的道具。我可以将转换器添加到父对象,但由于两个原因,它会导致问题。

  1. 大多数字段都映射到简单属性,我不想编写代码来手动反序列化和序列化所有字段。
  2. “prop_xx”字段的约定出现在多个对象中,我不想为每个对象编写JsonConverters。
  3. 我的想法是让所有对象实现一个接口IHasProps,然后编写IHasPropsJsonConverter。转换器会尝试使用内置功能来读取和写入道具,除非在写入时遇到指示其prop对象的类型的属性,并且在读取时遇到与模式^prop\d+$匹配的字段。

    这似乎是 overkill 。还有更好的方法吗?

1 个答案:

答案 0 :(得分:2)

Your approach of using a converter should work but is a little tricky to do in a generic fashion without getting a stack overflow exception. For writing, see Generic method of modifying JSON before being returned to client for one way of doing it. For reading, you can load into a JObject, populate the regular properties along the lines of Json.NET custom serialization with JsonConverter - how to get the “default” behavior then identify and parse the "prop_XXX" properties. Note that these solutions don't play well with TypeNameHandling or PreserveReferencesHandling.

However, a simpler approach may be to make use of [JsonExtensionData] to temporarily store your variable set of properties in a IDictionary<string, JToken> during the serialization process and then add them to the List<string> Props when serialization is complete. This can be done using serialization callbacks:

public class Parent
{
    public Parent() { this.Props = new List<string>(); }

    [JsonProperty("id")]
    public int ParentId { get; set; }

    [JsonProperty("value")]
    public string Value { get; set; }

    [JsonIgnore]
    public List<string> Props { get; set; }

    [JsonExtensionData]
    JObject extensionData; // JObject implements IDictionary<string, JToken> and preserves document order.

    [OnSerializing]
    void OnSerializing(StreamingContext ctx)
    {
        VariablePropertyListExtensions.OnSerializing(Props, ref extensionData, false);
    }

    [OnSerialized]
    void OnSerialized(StreamingContext ctx)
    {
        VariablePropertyListExtensions.OnSerialized(Props, ref extensionData, false);
    }

    [OnDeserializing]
    void OnDeserializing(StreamingContext ctx)
    {
        VariablePropertyListExtensions.OnDeserializing(Props, ref extensionData, false);
    }

    [OnDeserialized]
    void OnDeserialized(StreamingContext ctx)
    {
        if (Props == null)
            Props = new List<string>();
        VariablePropertyListExtensions.OnDeserialized(Props, ref extensionData, false);
    }
}

public static class VariablePropertyListExtensions
{
    public const string Prefix = "prop_";

    readonly static Regex regex;

    static VariablePropertyListExtensions()
    {
        regex = new Regex("^" + Prefix + @"\d+" + "$", RegexOptions.CultureInvariant | RegexOptions.Compiled); // Add  | RegexOptions.IgnoreCase if required
    }

    public static void OnSerializing<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
    {
        Debug.Assert(keepUnknownProperties || (extensionData == null || extensionData.Count == 0));

        // Add the prop_ properties.
        if (props == null || props.Count < 1)
            return;
        extensionData = extensionData ?? new TDictionary();
        for (int i = 0; i < props.Count; i++)
            extensionData.Add(Prefix + (i + 1).ToString(NumberFormatInfo.InvariantInfo), (JValue)props[i]);
    }

    internal static void OnSerialized<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
    {
        // Remove the prop_ properties.
        if (extensionData == null)
            return;
        foreach (var name in extensionData.Keys.Where(k => regex.IsMatch(k)).ToList())
            extensionData.Remove(name);
        // null out extension data if no longer needed
        if (!keepUnknownProperties || extensionData.Count == 0)
            extensionData = null;
    }

    internal static void OnDeserializing<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
    {
        Debug.Assert(keepUnknownProperties || (extensionData == null || extensionData.Count == 0));
    }

    internal static void OnDeserialized<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
    {
        props.Clear();
        if (extensionData == null)
            return;
        foreach (var item in extensionData.Where(i => regex.IsMatch(i.Key)).ToList())
        {
            props.Add(item.Value.ToObject<string>());
            extensionData.Remove(item.Key);
        }
        // null out extension data if no longer needed
        if (!keepUnknownProperties || extensionData.Count == 0)
            extensionData = null;
    }
}

Here I have moved the logic for populating and deserializing the extension data dictionary to a helper class for reuse in multiple classes. Note that I am adding the "prop_XXX" properties to the properties list in document order. Since the standard states that a JSON object is an unordered set of key/value pairs, for added robustness you might want to sort them by their XXX index.

Sample fiddle.