dev-resources.site
for different kinds of informations.
JSON#: nuestro propio lector/escritor
En la anterior entrada, ten铆amos una peque帽a estructura de clases con composici贸n entre ellas. Utilizar JSON para guardar estos datos de forma autom谩tica, mediante el serializador, era imposible, ya que para empezar ten铆amos un ciclo entre las referencias, y adem谩s, el tener propiedades de solo lectura hac铆a que la API JSON no recuperarse ninguna informaci贸n. Tambi茅n vimos un par de soluciones sencillas para estos problemas.
Esta es la soluci贸n compleja.
La API JSON de C# contempla el caso en el que nuestra clase resulte tan compleja de guardar que necesitamos nuestros propios mecanismos, totalmente personalizados. Para esto, podemos hacer derivar la clase JsonConverter, donde T
es la clase de la que queremos guardar los objetos en formato JSON.
Un ejemplo sencillo
Un ejemplo sencillo podr铆a ser una clase que incorpore fechas, de manera que utilice el formato de fecha local, pero la guarde con el formato ISO 8601, es decir YYYY-MM-DD (donde 'Y' es un d铆gito para el a帽o, 'M' para el mes, y 'D' para el d铆a). De hecho, si intentamos serializar la siguiente clase, nos dar谩 error diciendo que la clase DateOnly no sabe c贸mo serializarla.
public class EntradaDiario {
public required DateOnly Fecha { get; init; }
public required string Texto { get; init; }
public override string ToString()
{
return $"{this.Fecha}: {this.Texto}";
}
}
As铆 que tenemos que crear una clase conversora, que explique a la API JSON c贸mo serializar EntradaDiario.
class EntradaDiarioConverter: JsonConverter<EntradaDiario> {
public override DateTimeOffset Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
// m谩s cosas...
}
public override void Write(
Utf8JsonWriter writer,
EntradaDiario entrada,
JsonSerializerOptions options)
{
// m谩s cosas...
}
}
B谩sicamente, tenemos que explicar c贸mo leer y escribir una EntradaDiario. Lo m谩s sencillo es escribir JSON, es decir, completar el m茅todo Write().
class EntradaDiarioConverter: JsonConverter<EntradaDiario> {
// m谩s cosas...
public override void Write(
Utf8JsonWriter writer,
EntradaDiario entrada,
JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString( "fecha", $"{entrada.Fecha.Year, 4:D4}-{entrada.Fecha.Month, 2:D2}-{entrada.Fecha.Day, 2:D2}" );
writer.WriteString( "texto", entrada.Texto );
writer.WriteEndObject();
}
}
Los m茅todos de JsonWriter WriteStartObject() y WriteEndObject() escriben las llaves: '{}'. En el medio, escribimos dos cadenas de caracteres con WriteString(), la primera para la fecha con el formato ISO (a la que le damos el nombre 麓fecha'), y la segunda para el texto (que recibe el nombre 麓texto'), escribiendo una entrada del diario completa.
La salida es como sigue.
{"fecha":"2024-12-03","texto":"Prueba"}
La parte de lectura es m谩s compleja, como ya anunci谩bamos m谩s arriba. Ser谩 necesario leer el texto de la entrada y la fecha, pero no sabemos en qu茅 orden vendr谩 (no podemos asumir que solo lo leeremos habiendo sido escrito por nosotros). Deberemos saber si hemos le铆do o no una EntradaDiario y en caso negativo devolver null.
class EntradaDiarioConverter...
// m谩s cosas...
public EntradaDiario? Read(...
string texto = "";
DateOnly? fecha = null;
EntradaDiario? toret = null;
while ( reader.Read() ) {
// leer la fecha
// leer el texto
}
if ( fecha != null ) {
toret = new EntradaDiario{ Fecha = (DateOnly) fecha, Texto = texto };
}
return toret;
Debemos tratar de leer el texto de la entrada y la fecha. Lo hacemos en un bucle para evitar imponer un orden de lectura. Entonces al salir del bucle, comprobamos si al menos hemos le铆do la fecha. En ese caso, se crea una objeto entrada que se devuelve, en caso contrario, se va a devolver null. A continuaci贸n, el interior del bucle.
// m谩s cosas...
while ( reader.Read() ) {
var tokenType = reader.TokenType;
if ( tokenType == JsonTokenType.PropertyName
&& reader.ValueTextEquals( "fecha" ) )
{
reader.Read();
fecha = DateOnly.ParseExact(
reader.GetString() ?? "",
new []{ "yyyy-MM-dd" } );
}
if ( tokenType == JsonTokenType.PropertyName
&& reader.ValueTextEquals( "texto" ) )
{
reader.Read();
texto = reader.GetString() ?? "";
}
}
Se utilizan los m茅todos del lector Read(), que lee el siguiente token. Este m茅todo se utiliza en conjunci贸n con la propiedad TokenType para saber si estamos ante el nombre de una propiedad (TokenType.PropertyName) y un m茅todo (ValueTextEquals()), y en caso de reconocer uno de las propiedades a leer, tomamos su valor. En ambos camos leemos una cadena de caracteres, por lo que llamamos a GetString(). Este m茅todo devuelve null si no encuentra una cadena de caracteres, por lo que utilizamos el operador ?? para devolver un cadena vac铆a en caso de que se produzca ese error. En el caso de la fecha, deberemos realizar un paso extra, que consistir谩 en llamar a ParseExact() para leer la fecha desde el formato ISO en el que fue guardado.
Aplic谩ndolo a la biblioteca
En el caso de nuestra anterior entrada, necesitamos crear conversores para las clases Biblioteca y Autor, de forma que trate de manera especial esas colecci贸n de objetos Libro en Autor, y los objetos Autor en Biblioteca.
Ya que el guardado funciona, no proporcionaremos el m茅todo Write(), sino tan solo Read(). Por desgracia, como hemos visto es el m谩s complejo de los dos.
Lo que tenemos que hacer para la clase Biblioteca es leer los objetos Autor, y utilizar el m茅todo Biblioteca.Inserta() para introducirlos dentro del objeto. De forma similar, para la clase Autor necesitamos leer los objetos Libro y llamar con ellos a Autor.Inserta().
En el caso del autor, tenemos que recuperar el nombre del mismo (propiedad Nombre), y su a帽o de nacimiento (propiedad AnnoNac). Finalmente, necesitamos recuperar una colecci贸n de objetos Libro. Afortunadamente, en este punto podemos utilizar JsonSerializer.Deserialize>(), de forma que no ser谩 necesario que nosotros procesemos toda la lista.
public class AutorJsonConverter: JsonConverter<Autor> {
public override Autor? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Autor? toret = null;
string nombre = "";
int annonac = 1451; // se inventa la imprenta
ICollection<Libro>? libros = new List<Libro>();
reader.Read();
while ( reader.TokenType != JsonTokenType.EndObject ) {
if ( reader.TokenType == JsonTokenType.PropertyName
&& reader.ValueTextEquals( "Nombre" ) )
{
reader.Read();
nombre = reader.GetString() ?? "";
}
else
if ( reader.TokenType == JsonTokenType.PropertyName
&& reader.ValueTextEquals( "AnnoNac" ) )
{
reader.Read();
annonac = reader.GetInt32();
}
else
if ( reader.TokenType == JsonTokenType.PropertyName
&& reader.ValueTextEquals( "Libros" ) )
{
libros = JsonSerializer.Deserialize<ICollection<Libro>>(ref reader, options);
toret = new Autor {
Nombre = nombre,
AnnoNac = annonac };
foreach (var libro in libros ?? new List<Libro>())
{
toret.Inserta( libro );
}
}
reader.Read();
}
return toret;
}
public override void Write(Utf8JsonWriter writer, Autor value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
Como no sabemos el orden en el que se leer谩n las propiedades, empleamos un bucle y una secuencia de if's para tratar las tres posibilidades: el nombre del autor, su a帽o de nacimiento, y la colecci贸n de libros. En el caso de leer la colecci贸n de libros, se crea el objeto Autor, pues el resto de datos se consideran accesorios.
Se crea entonces el objeto con el nombre y el a帽o de nacimiento le铆dos hasta el momento, se llama sucesivamente a Autor.Inserta() para introducir el libro en la colecci贸n del autor.
Algo muy parecido se hace para la clase Bibliteca, aunque esta vez se trata de leer el nombre de la biblitoeca y la colecci贸n de autores.
public class BibliotecaJsonConverter: JsonConverter<Biblioteca> {
public override Biblioteca? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Biblioteca? toret = null;
string nombre = "";
ICollection<Autor>? autores = new List<Autor>();
reader.Read();
if ( reader.TokenType == JsonTokenType.PropertyName
&& reader.ValueTextEquals( "Nombre" ) )
{
reader.Read();
nombre = reader.GetString() ?? "";
reader.Read();
autores = JsonSerializer.Deserialize<ICollection<Autor>>(ref reader, options);
toret = new Biblioteca { Nombre = nombre };
foreach (var autor in autores ?? new List<Autor>())
{
toret.Inserta( autor );
}
reader.Read(); // endobject
reader.Read();
}
return toret;
}
public override void Write(Utf8JsonWriter writer, Biblioteca value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
Como se puede ver, el funcionamiento es muy parecido al anterior.
Referencias
C贸digo fuente con guardado y lectura de JSON mediante JsonConverter para una entrada de un diario.
C贸digo fuente con guardado y lectura de JSON mediante JsonConverter para una biblioteca.
Entrada sobre uso de conversores para la API JSON en microsoft learn.
Featured ones: