Trabajando con documentos JSON en .NET Core 3.x (II)
Introducción
En una entrada anterior, veíamos de forma general algunos detalles con respecto al nuevo namespace System.Text.Json incorporado en .NET Core 3.x.
En esta ocasión, vamos a ver de forma práctica algunas de sus aplicaciones generales.
Agregando la referencia al namespace
Lo primero es que dentro de nuestro código, deberemos agregar una referencia al namespace que nos permite trabajar con documentos JSON:
using System.Text.Json;
Una vez hecho esto, podremos trabajar con las clases correspondientes al namespace que nos permitirán gestionar y trabajar con documentos JSON.
Serializar
Partiremos de una entidad Person cuyos miembros o propiedades son las siguientes:
public class Person { public string Name { get; set; } public DateTime Birthday { get; set; } }
Para serializar la entidad, utilizaremos la clase JsonSerializer.
El código demostrativo de ejemplo de una serialización tipo quedaría de la siguiente forma:
var person = new Person() { Name = "Jorge", Birthday = new DateTime(1999, 12, 31) }; var json = JsonSerializer.Serialize(person);
Deserializar
Para desarializar los datos de un documento JSON en una entidad, utilizaremos igualmente la clase JsonSerializer.
Un ejemplo demostrativo de este uso es el que se indica a continuación:
var json = "{\"Name\":\"Jorge\",\"Birthday\":\"1999-12-31T00:00:00\"}"; var person = JsonSerializer.Deserialize(json);
Aspectos avanzados deserializando
A veces, nos podemos encontrar con algunos comportamientos inicialmente no esperables cuando tratamos de deserializar un JSON.
A continuación expondré algunos de estos casos.
Case sensitive
Por defecto, la serialización y deserialización se realiza en modo PascalCase.
Imaginemos que el campo Birthday viene con la primera letra en minúscula en lugar de en mayúscula al puro estilo camelCase.
var json = "{\"Name\":\"Jorge\",\"birthday\":\"1999-12-31T00:00:00\"}";
Al deserializar nuestro JSON, sólo nos va a deserializar correctamente la información de Name, pero la información de Birthday la va a ignorar.
Para resolver esta problemática, podemos utilizar la clase JsonSerializerOptions en combinación con JsonSerializer.
JsonSerializerOptions nos proveerá opciones que podremos utilizar al serializar o deserializar usando JsonSerializer.
Por ejemplo.
Para resolver el problema de que nos venga un JSON cuyos atributos no concuerden con exactitud en mayúsculas y minúsculas con las propiedades de la entidad, bastará con configurarlo de la siguiente manera:
var options = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
var json = "{\"Name\":\"Jorge\",\"birthday\":\"1999-12-31T00:00:00\"}";
var person = JsonSerializer.Deserialize(json, options);
De esta forma, la deserialización se realizará ahora de forma correcta.
Forzando una deserialización en camelCase
Cabe reiterar como indicaba antes, que por defecto la serialización y deserialización se realiza en modo PascalCase por defecto.
Pero podríamos cambiar este comportamiento por defecto haciendo que la serialización y deserialización se realice en camelCase en lugar de en PascalCase utilizando nuevamente JsonSerializerOptions.
En este caso, escribiríamos las opciones de la siguiente forma:
var options = new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var json = "{\"Name\":\"Jorge\",\"birthday\":\"1999-12-31T00:00:00\"}";
var person = JsonSerializer.Deserialize(json, options);
Aquí, Name aparecería como null, mientras que Birthday aparecería correctamente deserializado.
Aspectos avanzados serializando
También podemos hacer uso de diferentes opciones serializando al igual que hemos visto al deserializar.
La forma de hacer esto es exactamente igual.
Veamos algunas de estas interesantes características.
Ignorar null
Cuando serializamos, es posible que nos interese ignorar aquellos datos que tienen un valor null.
Para ello, bastará con indicar en las opciones de serialización, la propiedad IgnoreNullValues a true, ya que por defecto es false.
Así que nuestro código podría quedar de la siguiente forma:
var person = new Person() { Birthday = DateTime.UtcNow };
var options = new JsonSerializerOptions() { IgnoreNullValues = true };
var json = JsonSerializer.Serialize(person, options);
Json sangrado
Imaginemos que tenemos el siguiente JSON:
{ "Name": "Jorge", "Birthday": "1999-12-31T00:00:00" }
Y queremos presentarlo en pantalla o en fichero como:
{ "Name": "Jorge", "Birthday": "1999-12-31T00:00:00" }
Pues bien, la forma de lograr esto es nuevamente con las opciones a la hora de serializar la información.
Nuestro código en este caso, quedará de la siguiente forma:
var person = new Person() { Name = "Jorge", Birthday = new DateTime(1999, 12, 31) };
var options = new JsonSerializerOptions() { WriteIndented = true };
var json = JsonSerializer.Serialize(person, options);
Ignorar propiedades de sólo lectura
Imaginemos que la entidad que hemos estado desde el principio, es de esta otra forma:
public class Person { public string Name { get; set; } public DateTime Birthday { get; set; } public string City { get; private set; } = "FooCity"; }
Es decir, tiene un propiedad de sólo lectura con un valor fijo, y queremos que omitir esos valores fijos de sólo lectura al serializar nuestro JSON.
En este caso, podríamos escribir nuestro código de la siguiente forma:
var person = new Person() { Name = "Jorge", Birthday = new DateTime(1999, 12, 31) };
var options = new JsonSerializerOptions() { IgnoreReadOnlyProperties = true };
var json = JsonSerializer.Serialize(person, options);
Serializar en bytes UTF-8
Como mencionaba en otro momento de esta entrada, es posible trabajar con UTF-8 directamente.
De hecho, podemos devolver un conjunto de bytes resultado de la serialización de nuestro objeto en formato UTF-8.
Esto lo lograremos gracias a la clase SerializeToUtf8Bytes.
Serializar a UTF-8 es cerca de entre 5% y 10% más rápido debido a que los bytes (como UTF-8) no necesitan ser convertidos a cadenas (UTF-16).
Así que un ejemplo de este uso sería el siguiente:
var person = new Person() { Name = "Jorge", Birthday = new DateTime(1999, 12, 31) };
var json = JsonSerializer.SerializeToUtf8Bytes(person);
Deserializar desde bytes en formato UTF-8
El caso inverso especial es el que partiendo de un conjunto de bytes en formato UTF-8, queremos deserializarlo en su entidad de forma directa.
En este caso, y suponiendo que tenemos la variable anterior json que contiene el JSON serializado en bytes, podemos deserializar directamente éste de la forma:
var person = JsonSerializer.Deserialize(json);
JsonSerializer.Deserialize nos permite deserializar directamente el array de bytes de nuestro JSON inicial.
Otra aplicaciones
Ignorar propiedades
Es posible que estemos interesados en ignorar alguna propiedad a la hora de serializar o deserializar un JSON.
Imaginemos que partimos de la siguiente entidad de salida:
public class Person
{
public string Name { get; set; }
public DateTime Birthday { get; set; }
[JsonIgnore] public string City { get; set; };
}
La salida a la hora de serializar la información será un JSON sin la propiedad ignorada.
En el caso de deserializar la información, se ignorará igualmente esta propiedad, aunque en el JSON venga la información de la misma.
Cambiar el nombre de salida de una propiedad
Otra circunstancia con la que podemos tener que lidiar es la necesidad de cambiar el nombre de salida de una propiedad.
Una vez más, partimos de nuestra entidad que tendrá el siguiente aspecto:
public class Person
{
public string Name { get; set; }
public DateTime Birthday { get; set; }
[JsonPropertyName("Place")] public string City { get; set; };
}
Al serializar, obtendremos un JSON cuya propiedad City queda sustituida por Place.
Evitar perder datos al deserializar
En determinadas circunstancias, es posible que nos llegue un JSON con datos esperados y otros datos, que no contemplábamos inicialmente.
Si nuestro propósito es no perder esos datos que no contemplábamos inicialmente en el JSON de entrada de acuerdo a las propiedades de nuestra entidad, entonces podemos hacer uso del atributo JsonExtensionDataAttribute.
De esta forma y gracias a este atributo, podremos obtener los elementos del JSON en una colección de tipo Dictionary<string, object> o Dictionary<string, JsonElement>.
Veamos su uso con un ejemplo partiendo de la siguiente entidad:
public class Person
{
public string Name { get; set; }
public DateTime Birthday { get; set; }
public string City { get; set; }
[JsonExtensionData] public Dictionary<string, object> ExtensionData { get; set; }
}
La deserialización de la información de entrada en la que tenemos tres datos «esperados» y otros datos «inesperados» quedará de la siguiente forma:
var json = "{\"Name\":\"Jorge\",\"Birthday\":\"1999-12-31T00:00:00\"," + "\"City\":\"London\",\"Country\":\"UK\",\"Age\":\"30\"}"; var person = JsonSerializer.Deserialize(json);
De esta manera, no perderemos la información adicional no esperada que nos devuelve el JSON.
Creando nuestras propias políticas y opciones
Ahora bien, hemos visto que podemos serializar y deserializar documentos JSON gracias a System.Text.Json, y hemos visto como usar también opciones para llevar a cabo tareas adicionales con respecto a la serialización y deserialización.
Sin embargo, ¿qué ocurre si necesitamos aplicar alguna conversión u opción que no esté disponible de forma directa en la serialización y deserialización?.
Pues en este caso, tendremos que crearnos nuestra propia opción personalizada, algo que por supuesto, es posible hacer.
Para ello, deberemos crear una clase que deberá heredar a su vez de la clase JsonNamingPolicy, una clase abstracta de System.Text.Json.
Y dentro de esta clase, deberemos sobreescribir la función ConvertName.
De hecho, supongamos el siguiente sencillo ejemplo en el que vamos a convertir todas las propiedades en lowercase.
Nuestra clase especial podría quedar de la siguiente forma:
public class JsonLowerCaseNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name) => name.ToLower();
}
Y la aplicación de esta política, podría quedar de la siguiente forma:
var person = new Person() { Name = "Jorge", Birthday = new DateTime(1999, 12, 31) };
var options = new JsonSerializerOptions() { PropertyNamingPolicy = new JsonLowerCaseNamingPolicy() };
var json = JsonSerializer.SerializeToUtf8Bytes(person, options);
La salida de cada propiedad en este caso será en formato lowercase.
No obstante, algo que no he comentado hasta ahora, es que es posible combinar varias opciones a la vez al serializar y deserializar, separando por comas cada una de las propiedades de opciones a utilizar.
JsonDocument y DOM
Otra de las características que tenemos dentro de System.Text.Json es la posibilidad de crear un DOM (Document Object Model) del JSON de partida en modo lectura, y acceder a sus elementos.
Veamos esto con un sencillo ejemplo.
var json = "{\"Name\":\"Jorge\",\"Birthday\":\"1999-12-31T00:00:00\"," + "\"City\":\"London\",\"Country\":\"UK\",\"Age\":\"30\"}"; using (JsonDocument document = JsonDocument.Parse(json)) { JsonElement root = document.RootElement; Console.WriteLine($"{root.GetProperty("City")} ({root.GetProperty("Country")})"); }
Evidentemente, éste es un ejemplo muy sencillo de uso de JsonDocument y DOM que utilizaremos en determinadas circunstancias en las que a partir de un JSON queremos acceder a un elemento o recorrerlos de forma rápida y sencilla.
Conclusiones finales
Como podemos observar, no es nada muy distinto a lo que otras librerías JSON permiten hacer, por lo que la migración a este namespace, tampoco debe representar un coste elevado si no hemos realizado personalizaciones muy especiales.
Cabe destacar igualmente, que muchos de los métodos indicados en estos ejemplos, contienen sus correspondientes métodos asíncronos Async.
Espero que este pequeño repaso general ayude a la hora de aplicar System.Text.Json en nuestras aplicaciones.
Happy Coding!