0

I have a situation where an API has multiple array-like objects as individual properties on a object. For example:

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

I want the resulting model in C# to not repeat that same structure and have prop_X deserialized as a List and serialized back to that mess.

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

I tried added JsonConverter attribute to the Props property, but I couldn't figure out how to get the props I needed on the parent. I could add a converter to the parent object, but for two reasons it causes a problem.

  1. Most of the fields map to simple properties and I don't want to have to write the code to manually deserialize and serialize all of them.
  2. The convention of "prop_xx" fields appears in multiple objects and I'd hate to write JsonConverters for each object.

My idea was to have all the objects implement an interface, IHasProps, and write an IHasPropsJsonConverter. The converter would try to use as much as the built in functionality to read and write props, except when encountering an attribute on a type that indicates its a prop object when write, and a field that matches the pattern ^prop\d+$ when reading.

This seems like overkill. Is there a better way?

Daniel Gimenez
  • 18,530
  • 3
  • 50
  • 70

1 Answers1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • A few things you might want to consider changing in your code: * `(JValue)props[i]` produced a run time error, I changed to `new JValue(props[i])` * Get rid of `OnDeserializing` since that does nothing. * Get rid of `IList props` parameter from `OnSerialized` – Daniel Gimenez Aug 31 '16 at 20:52
  • **Great answer!** I moved the regex and prefix into the methods so I can use this for multiple collections. It even works when I have multiple collections in the same class, which was part of my use case but I didn't mention because I didn't want to muddy the question. I also pass a collection of `KeyValuePair` to `OnSerializing` and a setting action to `OnDeserializing`. This allowed for a lot more flexible implementation including handling the property values coming in out of order. – Daniel Gimenez Aug 31 '16 at 20:57
  • @DanielGimenez - thanks for the feedback. I'm not sure why `(JValue)props[i]` fails since it works [here](https://dotnetfiddle.net/GKW0Db). The explicit cast is on [`JToken`](http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Linq_JToken_op_Implicit_29.htm) though so maybe that's a problem in your environment. You *might* want to check for pre-existing `"prop_XXX"` tokens in `OnDeserializing` which is why I left it in. It could be removed though. Either way all your changes sound good. – dbc Aug 31 '16 at 21:07
  • 1
    The reason why `(JValue)props[i]` didn't work for me is that I had changed the props value type from `string` to `object`. – Daniel Gimenez Sep 01 '16 at 12:22