¿Puedo serializar propiedades anidadas a mi clase en una sola operación con Json.net?

Digamos que tengo un modelo como:

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

Y a Json les gusta esto:

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

He estado tratando de encontrar una forma de serializar mi objeto sin hacer un modelo para el “nombre” y un modelo para los “detalles” ya que esto no se adapta bien a mi base de datos, por lo que implica un poco de malabares para completar la clase.

Puedo hacer varias pasadas en JsonConvert.PopulateObject () como:

 var mod = new MyModel(); JsonConvert.PopulateObject(json.ToString(), mod); JsonConvert.PopulateObject(json["details"].ToString(), mod); 

Pero en mi código real estoy ejecutando múltiples hilos y PopulateObject no es seguro para subprocesos, bloquea la aplicación. Los comentarios de PopulateJsonAsync () dicen que no se debe usar, sino que se debe usar Task.Run () en PopulateObject ().

Esto no funciona y todavía bloquea la aplicación cuando la llamo así:

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

Algunos logran pasar, pero finalmente la aplicación se bloquea por completo. Si elimino PopulateObject todos los hilos terminan bien, así que estoy bastante seguro de que esta función no es segura para subprocesos.

¿Existe un enfoque prolijo y seguro para rellenar mi objeto en un solo paso?

Puedes hacerlo con el siguiente convertidor:

 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(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 : IDisposable { Action setValue; T oldValue; public PushValue(T value, Func getValue, Action 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 } 

Y luego úsalo así:

 [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(json); Debug.WriteLine(JsonConvert.SerializeObject(mod, Formatting.Indented)); } } 

El método ReadJson() es sencillo: deserializar a JObject , reestructurar las propiedades apropiadas y MyModel clase MyModel . WriteJson es un poco más incómodo; el convertidor necesita deshabilitarse temporalmente de manera segura para generar un JObject “predeterminado” que luego se puede reestructurar.

Simplemente puede usar su modelo con un campo adicional para details y usar el atributo JsonIgnore para ignorar la serialización de los campos Size y Weight . Entonces su modelo se verá así:

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

entonces puedes simplemente serializar / deserializar tu modelo así:

 var deserializedModel = JsonConvert.DeserializeObject("your json string..."); var myModel = new MyModel { Name = "widget", Size = new[] { "XL", "M", "S" }, Weight = "heavy" }; string serializedObject = JsonConvert.SerializeObject(myModel); 

Esto debería funcionar:

 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(reader); } else if (string.Equals("size", propertyName, StringComparison.OrdinalIgnoreCase)) { if (!inDetails) { throw new JsonSerializationException(); } reader.Read(); result.Size = serializer.Deserialize(reader); } else if (string.Equals("weight", propertyName, StringComparison.OrdinalIgnoreCase)) { if (!inDetails) { throw new JsonSerializationException(); } reader.Read(); result.Weight = serializer.Deserialize(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; } } 

Con el atributo en la clase, usarlo es tan fácil como:

 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(output); /* { Name = "widget", Size = [ "XL", "M", "S" ], Weight = "heavy" } */