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

最后都变了- 提交于 2019-12-17 05:13:14

问题


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?


回答1:


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.




回答2:


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);



回答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"
}
*/


来源:https://stackoverflow.com/questions/30175911/can-i-serialize-nested-properties-to-my-class-in-one-operation-with-json-net

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!