DEV Community

JSON#

La serialización no es nueva. La forma clásica consiste en guardar directamente el contenido de la memoria que señala la referencia a un objeto, concretamente el número de bytes que ocupa el mismo.

Esto tiene dos problemas: a) si utilizamos directamente los bytes en memoria, cualquier cambio de versión de .NET invalida los objetos guardados mediante esta técnica; y b) el objeto debería estar autocontenido para que esta técnica funcionara: cualquier referencia a otro objeto provocará un NullReferenceException.

Así que podemos utilizar XML o JSON. Esto es información estructurada utilizando texto como soporte. La serialización con XML es bastante compleja, puesto que pide que se cumplan bastantes precondiciones.

Vamos a tomar la siguiente clase Libro.

public class Libro {
    public enum Tipo { TapaBlanda, TapaDura, Bolsillo }

    public required Autor Autor { get; init; }
    public required string Titulo { get; init; }
    public required int Pags { get; init; }
    public required string Isbn { get; init; }
    public required Tipo Formato { get; init; }

    public override string ToString()
    {
        return $"{Autor.Nombre}: {Titulo} (ISBN: {Isbn}, {Pags} pgs.)";
    }
}
Enter fullscreen mode Exit fullscreen mode

Si creamos un libro:

var libro0 = new Libro {
        Titulo = "Don Quijote de la Mancha",
        Autor = new Autor { Nombre = "Cervantes"},
        Formato = Libro.Tipo.TapaDura,
        Isbn = "978-8420412146",
        Pags = 1376
};
Enter fullscreen mode Exit fullscreen mode

Un ejemplo de JSON podría ser el siguiente para el caso del Quijote de Cervantes:

{
    "Titulo": "Don Quijote de la Mancha",
    "Autor": { Nombre: "Cervantes" },
    "Formato": Libro.Tipo.TapaDura,
    "Isbn": "978-8420412146",
    "Pags": 1376
}
Enter fullscreen mode Exit fullscreen mode

Aquí veremos que JSON es un formato recursivo: es decir, un objeto se representa entre llaves, enumerando sus propiedades mediante parejas nombre_de_propiedad, valor. Si una propiedad es compleja, es decir, referencia a otro objeto, entonces abrimos llaves y volvemos al mismo esquema: enumerar sus propiedades nombre_de_propiedad, valor*.

Aunque hace algunos años había que utilizar una librería externa (la más popular era JSON.NET de NewtonSoft), hoy por hoy tenemos soporte para JSON (tieen su propia API), en la librería estándar. Emplearemos System.Text.Json y System.Text.Json.Serialize.

Proyecto Biblioteca

Nos falta código para completar nuestro proyecto de una biblioteca, y reafirmarnos en cómo complicarnos la vida. En esta solución, una biblioteca contiene autores, y un autor contiene libros. Véamoslo.

public class Autor {
    public Autor()
    {
        this.libros = new List<Libro>();
    }

    public required string Nombre { get; init; }
    public required int AnnoNac { get; init; }

    public Autor Inserta(Libro libro)
    {
        this.libros.Add( libro );
        return this;
    }

    public IList<Libro> Libros => this.libros.AsReadOnly();

    public override string ToString()
    {
        string infoAutor = $"{this.Nombre} ({this.AnnoNac})";
        return infoAutor + "\n" + string.Join( '\n', this.Libros);
    }

    private IList<Libro> libros;
}


public class Biblioteca {
    public Biblioteca()
    {
        this.autores = new List<Autor>();
    }

    public required string Nombre { get; init; }
    public IList<Autor> Autores => this.autores.AsReadOnly();

    public Biblioteca Inserta(Autor autor)
    {
        this.autores.Add( autor );
        return this;
    }

    public override string ToString()
    {
        return this.Nombre + "\n" + string.Join( '\n', this.Autores );
    }

    private IList<Autor> autores;
}
Enter fullscreen mode Exit fullscreen mode

De acuerdo, podemos crear una biblioteca ejemplificadora con el siguiente código.

var biblio = new Biblioteca { Nombre = "Universitaria UVigo" };
var julio = new Autor { Nombre = "Julio Verne", AnnoNac = 1828 };
var alex = new Autor { Nombre = "Alejandro Dumas", AnnoNac = 1802 };

biblio.Inserta( julio )
       .Inserta( alex );

var libro1 = new Libro {
                Titulo = "Viaje al centro de la tierra",
                Autor = julio,
                Formato = Libro.Tipo.Bolsillo,
                Isbn = "978-8468253572",
                Pags = 330 };

var libro2 = new Libro {
                Titulo = "Los tres mosqueteros",
                Autor = alex,
                Formato = Libro.Tipo.TapaBlanda,
                Isbn = "978-8491052401",
                Pags = 617 };

var libro3 = new Libro {
                Titulo = "La vuelta al mundo en ochenta días",
                Autor = julio,
                Formato = Libro.Tipo.TapaDura,
                Isbn = "978-8431662950",
                Pags = 384 };

julio.Inserta( libro1 )
      .Inserta( libro3 );
alex.Inserta( libro2 );

Console.WriteLine( biblio );
Enter fullscreen mode Exit fullscreen mode

La salida del programa es la siguiente:

Universitaria UVigo
Julio Verne (1828)
Julio Verne: Viaje al centro de la tierra (ISBN: 978-8468253572, 330 pgs.)
Julio Verne: La vuelta al mundo en ochenta días (ISBN: 978-8431662950, 384 pgs.)
Alejandro Dumas (1802)
Alejandro Dumas: Los tres mosqueteros (ISBN: 978-8491052401, 617 pgs.)
Enter fullscreen mode Exit fullscreen mode

Hasta aquí todo bien. Pero es el momento de serializar nuestro esquema. Toda esta importante información debe ser preservada, ¿no?

public class JsonBiblioteca {
    public required Biblioteca Biblioteca { get; init; }

    public void Save(string nf)
    {
        using var f = new FileStream( nf, FileMode.Create );
        JsonSerializer.Serialize( f, this.Biblioteca );
    }

    public static Biblioteca? Load(string nf)
    {
        using var f = new FileStream( nf, FileMode.Open );
        return JsonSerializer.Deserialize<Biblioteca>( f );
    }
}
Enter fullscreen mode Exit fullscreen mode

Y solo necesitamos una llamada como new JsonJsonBiblioteca{ Biblioteca = biblio }.Save( "biblio.json" );, para que se guarde toda la información. Fantástico.

...espera, espera... hay una excepción JsonException en la que ¡se queja de un ciclo en nuestros objetos! De hecho, si pensamos en las propiedades en nuestras clases (que es lo que se guarda), tenemos una biblioteca con una propiedad Autores, lo que nos lleva a una lista de objetos Autor. Cada objeto de esta clase tiene una propiedad Libros, que nos devuelve una lista de objetos Libro. Pero, y aquí está lo importante, la clase Libro tiene una propiedad Autor que referencia al autor del libro... aquí tenemos nuestra referencia cíclica. La API JSON de C# visita los objetos comenzando por el que indicamos al llamar a Serialize(), y después recorre los objetos necesarios siguiente sus propiedades. Esto se repite recursivamente, y por tanto: Biblioteca -> Autor -> Libro -> Autor -> Libro -> Autor...

Se nos podría ocurrir que podemos ignorar esta propiedad, decorándola con el atributo [JsonIgnore]. El problema es que es necesario aportarla en el momento de crear un Libro... ¡y la API JSON no sabrá de dónde sacarla!

Pero hay solución, afortunadamente. La API Json es capaz de detectar estos ciclos y guardar la información evitándolos. Tenemos que guardar los datos utilizando una opción, indicando que queremos preservar estos ciclos. Tendremos que mantener dicha opción no solo al guardar, sino también al cargar.

public class JsonBiblioteca {
    // más cosas...
    public void Save(string nf)
    {
        using var f = new FileStream( nf, FileMode.Create );
        var opts = new JsonSerializerOptions {
            ReferenceHandler = ReferenceHandler.Preserve
        };

        JsonSerializer.Serialize( f, this.Biblioteca, opts );
    }

    public static Biblioteca? Load(string nf)
    {
        using var f = new FileStream( nf, FileMode.Open );
        var opts = new JsonSerializerOptions {
            ReferenceHandler = ReferenceHandler.Preserve
        };

        return JsonSerializer.Deserialize<Biblioteca>( f, opts );
    }
}
Enter fullscreen mode Exit fullscreen mode

¡De acuerdo! Funciona. Menos mal...
Un momento, un momento... está pasando algo raro si cargamos el JSON generado y lo visualizamos...

// más cosas...
Console.WriteLine( biblio );
new JsonBiblioteca{ Biblioteca = biblio }.Save( "biblio.json" );

Console.WriteLine( "Lectura:" );
var biblio2 = JsonBiblioteca.Load( "biblio.json" );
Console.WriteLine( biblio2 );
Enter fullscreen mode Exit fullscreen mode

Obtenemos una salida... anticlimática...

Universitaria UVigo
Enter fullscreen mode Exit fullscreen mode

¡No carga prácticamente nada! Y digo que no carga porque, si abrimos el archivo, podemos comprobar que la información está ahí...

¿Qué está pasando? El problema tiene que ver con las propiedades de solo lectura. La API JSON asume que estas propiedades se calculan de alguna manera, mientras que tanto en Biblioteca y Autor, utilizamos los correspondientes métodos Inserta(), que permiten añadir autores y libros, respectivamente. Pero eso la librería JSON no lo sabe, claro.

El código ofensor es el siguiente:

class Autor {
    // más cosas...
    public IList<Libro> Libros => this.libros.AsReadOnly();
}
Enter fullscreen mode Exit fullscreen mode

El código en Biblioteca es muy similar a este, solo que se refiere a autores, no a libros.

Hay dos soluciones a este problema. La primera es la más sencilla, aunque implica la ligera modificación de la interfaz de estas dos clases. La segunda, permite no modificar las clases en absoluto, pero es mucho más compleja. Equilibrio, equilibrio...

La primera consiste en modificar ligeramente la interfaz de nuestra clase. Al fin y al cabo, si el problema es que no hay forma de escribir en estas propiedades, ya que son de solo lectura, entonces démosles la posibilidad de escritura. No tiene que ser una forma de escritura total, podemos limitarla al momento de la creación del objeto.

class Autor {
    // más cosas...
    public IList<Libro> Libros {
        get => this.libros.AsReadOnly();
        init {
            this.libros.Clear();
            foreach (var libro in value) {
                this.libros.Add( libro );
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Si hacemos lo mismo en la clase Biblioteca (pero con objetos Autor), tendremos el problema solucionado.

Sé lo que estás pensando. ¿Por qué vivir tranquilo cuando puedes vivir crispado? Efectivamente, quieres saber la solución difícil. Pero para eso tendrás que esperar un poco. ¿Podrás hacerlo?

Top comments (0)