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.
Top comments (0)