Guerrilla Guerrilla - 4 months ago 61
C# Question

Can I serialize nested properties to my class in one operation with Json.net?

Lets say I have a model like:

public class MyModel
{
public string Name { get; set; }
public string[] Size { get; set; }
public string Weight { get; set; }

}


And Json like this:

{
"name" : "widget",
"details" : {
"size" : [
"XL","M","S",
]
"weight" : "heavy"
}
}


I have been trying to work out a way to serialize my object without making one model for the "name" and one model for the "details" as this doesn't map nicely to my database so involves a little juggling to get class populated.

I can make multiple passes at JsonConvert.PopulateObject() like:

var mod = new MyModel();

JsonConvert.PopulateObject(json.ToString(), mod);
JsonConvert.PopulateObject(json["details"].ToString(), mod);


But in my real code I am running multiple threads and PopulateObject is not thread safe, it jams the app. The comments for PopulateJsonAsync() say not to use it but instead to use Task.Run() on PopulateObject().

This does not work and still locks the app when I call it like:

await Task.Run(() => JsonConvert.PopulateObject(response.ToString(), productDetail));

if (response["results"].HasValues)
{
await Task.Run(() => JsonConvert.PopulateObject(response["results"][0].ToString(), productDetail));
}


A few get through but eventually the app gets completely thread locked. If I remove PopulateObject the threads all terminate fine so I am pretty sure this function is not thread safe.

Is there a neat threadsafe approach to populating my object in a single step?

dbc dbc
Answer

You can do it with the following converter:

public class MyModelConverter : JsonConverter
{
    [ThreadStatic]
    static bool cannotWrite;

    // Disables the converter in a thread-safe manner.
    bool CannotWrite { get { return cannotWrite; } set { cannotWrite = value; } }

    public override bool CanWrite { get { return !CannotWrite; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(MyModel).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = JObject.Load(reader);
        obj.SelectToken("details.size").MoveTo(obj);
        obj.SelectToken("details.weight").MoveTo(obj);
        using (reader = obj.CreateReader())
        {
            // Using "populate" avoids infinite recursion.
            existingValue = (existingValue ?? new MyModel());
            serializer.Populate(reader, existingValue);
        }
        return existingValue;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // Disabling writing prevents infinite recursion.
        using (new PushValue<bool>(true, () => CannotWrite, val => CannotWrite = val))
        {
            var obj = JObject.FromObject(value, serializer);
            var details = new JObject();
            obj.Add("details", details);

            obj["size"].MoveTo(details);
            obj["weight"].MoveTo(details);
            obj.WriteTo(writer);
        }
    }
}

public static class JsonExtensions
{
    public static void MoveTo(this JToken token, JObject newParent)
    {
        if (newParent == null)
            throw new ArgumentNullException();
        if (token != null)
        {
            if (token is JProperty)
            {
                token.Remove();
                newParent.Add(token);
            }
            else if (token.Parent is JProperty)
            {
                token.Parent.Remove();
                newParent.Add(token.Parent);
            }
            else
            {
                throw new InvalidOperationException();
            }
        }
    }
}

public struct PushValue<T> : IDisposable
{
    Action<T> setValue;
    T oldValue;

    public PushValue(T value, Func<T> getValue, Action<T> setValue)
    {
        if (getValue == null || setValue == null)
            throw new ArgumentNullException();
        this.setValue = setValue;
        this.oldValue = getValue();
        setValue(value);
    }

    #region IDisposable Members

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
    public void Dispose()
    {
        if (setValue != null)
            setValue(oldValue);
    }

    #endregion
}

And then use it like this:

[JsonConverter(typeof(MyModelConverter))]
public class MyModel
{
    [JsonProperty("name")]
    public string Name { get; set; }
    [JsonProperty("size")]
    public string[] Size { get; set; }
    [JsonProperty("weight")]
    public string Weight { get; set; }
}

public class TestClass
{
    public static void Test()
    {
        string json = @"{
            ""name"" : ""widget"",
            ""details"" : {
                ""size"" : [
                    ""XL"",""M"",""S"",
                ],
                ""weight"" : ""heavy""
            }
        }";
        var mod = JsonConvert.DeserializeObject<MyModel>(json);
        Debug.WriteLine(JsonConvert.SerializeObject(mod, Formatting.Indented));
    }
}

The ReadJson() method is straightforward: deserialize to a JObject, restructure the appropriate properties, then populate the MyModel class. WriteJson is a little more awkward; the converter needs to temporarily disable itself in a thread-safe manner to generate a "default" JObject that can be then restructured.