Deserializar propiedades JSON dentro de un JSON

¡Hola!
El otro día se dio el caso de que tuve que deserializar un JSON dentro de un JSON, es decir, dado un JSON, una de las propiedades es un string que contiene otro JSON. No tiene pinta de que sea una gran forma de crear JSON, pero cuando se da el caso hay que deserializarlos 🙂

El JSON en concreto podría tener un aspecto como el siguiente:

{  
   "id":1,
   "name":"Cake",
   "jsonCharacteristics":"{\"id\":1001,\"topping\":\"Powdered Sugar\",\"batter\":\"Chocolate\"}"
}

Como podemos ver, la propiedad jsonCharacteristics contiene un string que tiene la estructura de un JSON. Para deserializarlo vamos a usar Newtonsoft.Json el estandar de facto para .NET. En primer lugar vamos a modelar nuestras dos clases:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public class Dessert
    {
        [JsonProperty("id")]
        public int Id { get; set; }
 
        [JsonProperty("name")]
        public string Name { get; set; }       
 
        [JsonProperty("jsonCharacteristics")]
        public Characteristics Characteristics { get; set; }
    }
 
    public class Characteristics
    {
        [JsonProperty("id")]
        public int Id { get; set; }
 
        [JsonProperty("topping")]
        public string Topping { get; set; }
 
        [JsonProperty("batter")]
        public string Batter { get; set; }
    }

El problema que si intentamos deserializar directamente lo anterior, nos generará una excepción:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    [TestFixture]
    class JsonTest
    {
        [Test]
        public void Internal_String_Json_Property_Is_Deserialize()
        {
            string jsonDonut = this.GivenAJsonDonut();
 
            var donut = JsonConvert.DeserializeObject<Dessert>(jsonDonut);
 
            Assert.AreEqual(1001,donut.Characteristics.Id);
            Assert.AreEqual("Powdered Sugar", donut.Characteristics.Topping);
            Assert.AreEqual("Chocolate", donut.Characteristics.Batter);
        }
 
        private string GivenAJsonDonut()
        {
            return "{" +
                   "\"id\":1," +
                   "\"name\":\"Cake\"," +
                   "\"jsonCharacteristics\":\"{\\\"id\\\":1001,\\\"topping\\\":\\\"Powdered Sugar\\\",\\\"batter\\\":\\\"Chocolate\\\"}\"" +
                   "}";
        }

Al ejecutar el test nos dirá:

Newtonsoft.Json.JsonSerializationException : Error converting value «{«id»:1001,»topping»:»Powdered Sugar»,»batter»:»Chocolate»}» to type ‘JsonSample.Characteristics’. Path ‘jsonCharacteristics’, line 1, position 115.
—-> System.ArgumentException : Could not cast or convert from System.String to JsonSample.Characteristics.

Vamos, que no puede convertir un string en un Characteristics. Para arreglar esto podemos utilizar una forma no demasiado bonita:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    public class Dessert
    {
        [JsonProperty("id")]
        public int Id { get; set; }
 
        [JsonProperty("name")]
        public string Name { get; set; }
 
        private string jsonCharacteristics;
        [JsonProperty("jsonCharacteristics")]
        public string JsonCharacteristics
        {
            get { return jsonCharacteristics; }
            set
            {
                if(!string.IsNullOrEmpty(value))
                    Characteristics = JsonConvert.DeserializeObject<Characteristics>(value);
                jsonCharacteristics = value;
            }
        }
 
        public Characteristics Characteristics { get; private set; }
    }
 
    public class Characteristics
    {
        [JsonProperty("id")]
        public int Id { get; set; }
 
        [JsonProperty("topping")]
        public string Topping { get; set; }
 
        [JsonProperty("batter")]
        public string Batter { get; set; }
    }

Lo único que estamos haciendo es crear esa propiedad string que nos reclama y, cuando se establece el valor, deserializar ese string en la propiedad Characteristics que nos interesa. Si ejecutamos el test anterior lo pasa sin problema. Si esto lo hacemos en un único punto de la aplicación y no queremos profundizar mucho en Newtonsoft.Json puede ser una opción perfectamente razonable, pero Newtonsoft.Json nos proporciona una herramienta para poder hacer esto, JsonConverter personalizados. Con esto tenemos la opción de generar un JsonConverter que se adapte a lo que necesitamos, en nuestro caso queremos que una propiedad string la deserialice en el tipo que necesitemos. Vamos a generar nuestro JsonConverter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public class StringJsonConverter : JsonConverter
    {
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            return JsonConvert.DeserializeObject(reader.Value.ToString(), objectType);
        }
 
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var json = JsonConvert.SerializeObject(value).Replace("\"", "\\\"");
            writer.WriteRawValue(string.Concat("\"", json, "\""));
        }
        public override bool CanConvert(Type objectType)
        {
            return true;
        }
    }

Como vemos, realmente estamos haciendo lo mismo que anteriormente, simplemente deserializar el string al tipo que queremos, pero de esta forma lo podemos reutilizar en cualquier punto de nuestra aplicación. Además hemos añadido la operación contraria, la de serializar el objeto a JSON. Lo único que tenemos que hacer ahora es especificar que queremos utilizar nuestro JsonConverter personalizado en la propiedad Characteristics:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public class Dessert
    {
        [JsonProperty("id")]
        public int Id { get; set; }
 
        [JsonProperty("name")]
        public string Name { get; set; }
 
        [JsonConverter(typeof(StringJsonConverter))]        [JsonProperty("jsonCharacteristics")]
        public Characteristics Characteristics { get; set; }
    }
 
    public class Characteristics
    {
        [JsonProperty("id")]
        public int Id { get; set; }
 
        [JsonProperty("topping")]
        public string Topping { get; set; }
 
        [JsonProperty("batter")]
        public string Batter { get; set; }
    }

Vamos a añadir también un test para comprobar que Characteristics se serializa correctamente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    [TestFixture]
    class JsonTest
    {
        [Test]
        public void Internal_String_Json_Property_Is_Deserialized()
        {
            string jsonDonut = this.GivenAJsonDonut();
 
            var donut = JsonConvert.DeserializeObject<Dessert>(jsonDonut);
 
            Assert.AreEqual(1001,donut.Characteristics.Id);
            Assert.AreEqual("Powdered Sugar", donut.Characteristics.Topping);
            Assert.AreEqual("Chocolate", donut.Characteristics.Batter);
        }
 
        [Test]
        public void Internal_Property_Is_Serialized_To_Json_String()
        {
            var donut = this.GivenADonut();
            string jsonDonutExpected = this.GivenAJsonDonut();
 
            var donutSerialized = JsonConvert.SerializeObject(donut);
 
            Assert.AreEqual(jsonDonutExpected, donutSerialized);
        }
 
 
        private string GivenAJsonDonut()
        {
            return "{" +
                   "\"id\":1," +
                   "\"name\":\"Cake\"," +
                   "\"jsonCharacteristics\":\"{\\\"id\\\":1001,\\\"topping\\\":\\\"Powdered Sugar\\\",\\\"batter\\\":\\\"Chocolate\\\"}\"" +
                   "}";
        }
        private Dessert GivenADonut()
        {
            return new Dessert()
            {
                Id = 1,
                Name = "Cake",
                Characteristics = new Characteristics()
                {
                    Id = 1001,
                    Topping = "Powdered Sugar",
                    Batter = "Chocolate"
                }
            };
        }
    }

Si los ejecutamos podemos ver que pasan los dos sin problema. Espero que os sea de utilidad 🙂
¡Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *