6

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?

Guerrilla
  • 13,375
  • 31
  • 109
  • 210
  • Sounds like you need [a custom `JsonConverter` class](http://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm). – Richard Deeming May 11 '15 at 19:50
  • I looked at that but couldnt see how to do nested property. You get a value in and can transform how its outputted but I am not sure how use converter to make it look in nested properties. I couldn't find any real documentation on it just a sample snippet. – Guerrilla May 11 '15 at 19:58
  • 1) `PopulateObject` and `DeserializeObject` should be thread-safe, see https://stackoverflow.com/questions/28095244/jsonconvert-deserializeobject-index-was-outside-the-bounds-of-the-array/28095245#28095245. Are you sure you're not maintaining some static list somewhere, or accessing your `MyModel` class from multiple threads simultaneously? 2) Why use `PopulateObject` instead of `DeserializeObject`? – dbc May 11 '15 at 21:18
  • I just wanted to keep my classes simple. When I get a lot of nested values they get long and it starts to get awkward with all the different dependencies. Passing over populate several times lets me just add the logic to my specific function for the operation instead of having to modify the class. Id rather keep class simple if I can. Deserialize is threadsafe but PopulateObject seems to be locking app as when I remove it things work fine. I might have this wrong, I will have a go at isolating locking issue. – Guerrilla May 11 '15 at 21:28
  • It would be possible to write a `JsonConverter` to handle the restructuring, but it requires you to use `DeserializeObject`, since `PopulateObject` doesn't call the converter for the root object. Would that be useful? – dbc May 11 '15 at 21:32
  • Yes it would be. I have been trying to learn how the converter works but I haven't found any clear guides. You were right though, PopulateObject is thread safe, I ran test and it had no issues, there must be a different reason it is locking so I am looking into this. I don't have to use populate object, I'd rather use deserialize, I just chose to use it as I don't understand the converters yet. – Guerrilla May 11 '15 at 22:00
  • @Guerrilla just to be clear, you can't change your model? Because I'm a little confused by what your problem is. If your model correctly represented that json schema then it would be a one line operation. If it's not possible to change I would do it in two lines but I'd still just use `JsonConvert.DeserializeObject` and assign the returned value to the appropriate property on `MyModel`. Also, no need to obfuscate the behavior with an implementation of `JsonConverter` a helper method with the 2-3 lines of code in it would do just fine... – evanmcdonnal May 11 '15 at 23:24

3 Answers3

8

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 1
    Thanks, I really appreciate that. Going to spend some time going through it and figuring out how it works. – Guerrilla May 11 '15 at 23:22
3

This should work:

public class MyModelJsonConverter : JsonConverter
{
    public override bool CanRead
    {
        get { return true; }
    }

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (objectType != typeof(MyModel))
        {
            throw new ArgumentException("objectType");
        }

        switch (reader.TokenType)
        {
            case JsonToken.Null:
            {
                return null;
            }
            case JsonToken.StartObject:
            {
                reader.Read();
                break;
            }
            default:
            {
                throw new JsonSerializationException();
            }
        }

        var result = new MyModel();
        bool inDetails = false;

        while (reader.TokenType == JsonToken.PropertyName)
        {
            string propertyName = reader.Value.ToString();
            if (string.Equals("name", propertyName, StringComparison.OrdinalIgnoreCase))
            {
                reader.Read();
                result.Name = serializer.Deserialize<string>(reader);
            }
            else if (string.Equals("size", propertyName, StringComparison.OrdinalIgnoreCase))
            {
                if (!inDetails)
                {
                    throw new JsonSerializationException();
                }

                reader.Read();
                result.Size = serializer.Deserialize<string[]>(reader);
            }
            else if (string.Equals("weight", propertyName, StringComparison.OrdinalIgnoreCase))
            {
                if (!inDetails)
                {
                    throw new JsonSerializationException();
                }

                reader.Read();
                result.Weight = serializer.Deserialize<string>(reader);
            }
            else if (string.Equals("details", propertyName, StringComparison.OrdinalIgnoreCase))
            {
                reader.Read();

                if (reader.TokenType != JsonToken.StartObject)
                {
                    throw new JsonSerializationException();
                }

                inDetails = true;
            }
            else
            {
                reader.Skip();
            }

            reader.Read();
        }
        if (inDetails)
        {
            if (reader.TokenType != JsonToken.EndObject)
            {
                throw new JsonSerializationException();
            }

            reader.Read();
        }

        return result;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        if (value == null)
        {
            writer.WriteNull();
            return;
        }

        var model = value as MyModel;
        if (model == null) throw new JsonSerializationException();

        writer.WriteStartObject();
        writer.WritePropertyName("name");
        writer.WriteValue(model.Name);
        writer.WritePropertyName("details");
        writer.WriteStartObject();
        writer.WritePropertyName("size");
        serializer.Serialize(writer, model.Size);
        writer.WritePropertyName("weight");
        writer.WriteValue(model.Weight);
        writer.WriteEndObject();
        writer.WriteEndObject();
    }
}

[JsonConverter(typeof(MyModelJsonConverter))]
public class MyModel
{
    public string Name { get; set; }
    public string[] Size { get; set; }
    public string Weight { get; set; }
}

With the attribute on the class, using it is as easy as:

var model = new MyModel
{
    Name = "widget",
    Size = new[] { "XL", "M", "S" },
    Weight = "heavy"
};

string output = JsonConvert.SerializeObject(model);
// {"name":"widget","details":{"size":["XL","M","S"],"weight":"heavy"}}

var model2 = JsonConvert.DeserializeObject<MyModel>(output);
/*
{
    Name = "widget",
    Size = [ "XL", "M", "S" ],
    Weight = "heavy"
}
*/
Richard Deeming
  • 29,830
  • 10
  • 79
  • 151
  • I want to deserialize from json to class. Is this documented anywhere? I can't find anywhere that explains in detail how custom converters work – Guerrilla May 11 '15 at 21:17
  • @Guerrilla: Sorry, I read *serialize my object* and thought you were trying to serialize *to* JSON, not deserialize *from* JSON. – Richard Deeming May 12 '15 at 09:52
  • @Guerrilla: Looks like you've already got an answer, but I've updated my code with an alternative `ReadJson` implementation. – Richard Deeming May 12 '15 at 10:09
3

You can simply use your model with an extra field for details and use JsonIgnore attribute to ignore serialization of Size and Weight fields. So your model will look like this:

public class MyModel
{
    [JsonProperty("name")]
    public string Name { get; set; }
    public Details details { get; set; }

    [JsonIgnore]
    public string[] Size
    {
        get
        {
            return details != null ? details.size : null;
        }
        set
        {
            if (details == null)
            {
                details = new Details();
            }
            details.size = value;
        }
    }

    [JsonIgnore]
    public string Weight
    {
        get
        {
            return details != null ? details.weight : null;
        }
        set
        {
            if (details == null)
            {
                details = new Details();
            }
            details.weight = value;
        }
    }
}

then you can simply serialize/deserialize you model like this:

var deserializedModel = JsonConvert.DeserializeObject<MyModel>("your json string...");
var myModel = new MyModel { Name = "widget", Size = new[] { "XL", "M", "S" }, Weight = "heavy" };
string serializedObject = JsonConvert.SerializeObject(myModel);
Arin Ghazarian
  • 5,105
  • 3
  • 23
  • 21
  • 3
    Or alternatively, have the `Weight` and `Size` be stored in the `MyModel` class, and have `Details` be a private nested class that simply passes the values through to appropriately named properties. – dbc May 11 '15 at 21:22