CodeGym /Cursos /C# SELF /Compatibilidad al cambiar la estructura de las clases

Compatibilidad al cambiar la estructura de las clases

C# SELF
Nivel 45 , Lección 4
Disponible

1. Introducción

Imagínate: escribiste la clase Person, serializaste un objeto a un archivo, y al cabo de un par de meses decidiste que necesita, digamos, una nueva dirección de residencia o cambiaste el tipo de algunas propiedades. Parece algo normal — pero cuando intentas cargar (deserializar) datos antiguos, puedes llevarte una sorpresa: algo no se cargará, se lanzará una excepción, algunos valores quedarán vacíos o incluso incorrectos.

Ese comportamiento es un caso típico de romper la compatibilidad hacia atrás. En desarrollo real esto pasa más a menudo de lo que los estudiantes olvidan poner un punto y coma (o sea, muy a menudo).

Presentemos el problema con un ejemplo

Veamos nuestro mini-proyecto de práctica. Supongamos que en esta etapa teníamos esta clase:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Serializamos una instancia de esta clase a JSON:

Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);

Obtuvimos en el archivo:

{"Name":"Alice","Age":35}

Ahora, una semana después, decidimos modernizar la aplicación agregando un campo de dirección:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; } // campo nuevo
}

Y luego — intentamos cargar el archivo viejo:

string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);

¿Qué pasará? No tendremos la dirección en nuestro objeto: la propiedad Address será igual a null. No surgió ningún error. Por ahora todo funciona... Pero si empiezas a cambiar tipos, eliminar campos o hacer algo realmente "interesante", ¡pueden surgir problemas!

2. Qué tipos de cambios existen — y cómo afectan

Los cambios en la estructura de las clases afectan la serialización de distintas maneras. Vamos a repasar varios escenarios típicos.

Agregar nuevas propiedades

Esta es la opción menos peligrosa. Los datos antiguos (donde no existían esas propiedades) se deserializan sin problemas: las nuevas propiedades tomarán los valores por defecto (null para referencias, 0 para int, etc.).

Atención: Si tu nueva propiedad no es nullable y no tiene un valor por defecto "razonable", puede haber problemas (especialmente con propiedades required en C# 11+).

Eliminar propiedades

Si eliminas una propiedad y en los datos serializados aún está presente — el serializador muy probablemente simplemente ignorará lo "extra" y la carga seguirá funcionando.

Pero esto depende del serializador. Por ejemplo, JsonSerializer y Newtonsoft.Json son bastante tolerantes: no lanzan excepción; sin embargo, algunos serializadores antiguos o personalizados pueden comportarse de otra forma.

Renombrar propiedades

Aquí empieza la diversión. Si simplemente renombras la propiedad FirstName a Name, el serializador no podrá mapear los campos de los datos antiguos al nuevo objeto. Dicho de otro modo, el campo quedará vacío (null/0) y lo antiguo en el archivo será ignorado.

Cambiar el tipo de una propiedad

Por ejemplo, antes tenías public int Age, luego decidiste hacerlo public string Age (por si alguien escribe "immortal" — de todo pasa). Intentar deserializar datos antiguos puede dar lugar a un error ("Cannot convert number to string") o la propiedad simplemente obtendrá el valor por defecto. Todo depende del serializador y de sus opciones de tipado estricto.

Cambiar jerarquías (herencia, anidamiento)

Si cambias clases base, mueves propiedades a otros lugares o, por ejemplo, haces una clase wrapper para otra — los datos serializados antiguos pueden volverse totalmente incompatibles. Esto afecta especialmente a XML y a jerarquías de objetos complejas.

3. Problemas de compatibilidad

¿Cómo detectar problemas de compatibilidad?

A menudo el error de compatibilidad no se manifiesta de inmediato ni de forma obvia: la aplicación empieza a comportarse "raro", parte de los datos se pierde, o en los logs aparece una excepción poco informativa. Más comúnmente los problemas afloran cuando:

  • El usuario carga un archivo antiguo en la nueva versión del programa.
  • El servidor recibe JSON/XML de un cliente de "versión antigua".
  • Trabajas con una API externa cuyo contrato actualizaron de repente.

Los síntomas varían: desde errores en la deserialización hasta campos "inesperadamente" vacíos.

Influencia del serializador en la compatibilidad

No todos los serializadores se comportan igual. Los serializadores JSON suelen ser más "permisivos" ante cambios — tanto el estándar System.Text.Json como Newtonsoft.Json. Suelen omitir propiedades desconocidas del archivo y no serializan de vuelta campos desconocidos del objeto.

En XML todo es un poco más estricto: si cambia el elemento raíz o la jerarquía, pueden surgir errores.

En formatos binarios puede incluso lanzarse una excepción si cambian el orden o los tipos.

4. Cómo minimizar riesgos — enfoques y prácticas

Aquí van varios enfoques que ayudan a reducir al mínimo los problemas (y a veces evitarlos por completo).

Usa versiones de clases y datos

Añade un campo especial Version en los objetos serializables o en los archivos. Esto permite determinar con qué versión de la estructura se creó el archivo y tomar decisiones al cargar (por ejemplo, ejecutar una actualización de datos).

public class PersonV2
{
    public int Version { get; set; } = 2;
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
}

Aplica atributos de mapeo de nombres (para serialización)

Con JSON y XML puedes indicar explícitamente cómo debe llamarse la propiedad en la forma serializada. Si renombras una propiedad — conserva el nombre antiguo:

public class Person
{
    [JsonPropertyName("FirstName")] // para System.Text.Json
    [JsonProperty("FirstName")]     // para Newtonsoft.Json
    public string Name { get; set; }
    public int Age { get; set; }
}

Usa tipos nullable y valores por defecto

Si te aparecen campos nuevos que no siempre existen en datos antiguos — hazlos nullable o asígnales un valor por defecto, para que la deserialización no dé problemas:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string? Address { get; set; } = "Unknown";
}

Manejo del evento "campo desconocido"

En Newtonsoft.Json puedes suscribirte al manejo de campos "no entendidos" mediante un manejador especial, para por ejemplo loguear la situación potencialmente peligrosa.

var settings = new JsonSerializerSettings
{
    MissingMemberHandling = MissingMemberHandling.Error
};
try
{
    var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
    Console.WriteLine("No se pudo deserializar: " + ex.Message);
}

Migración de datos

Si los cambios son sustanciales, es más sensato prever una etapa de migración: por ejemplo, cargar los datos en la estructura "antigua" y luego transformarlos a la nueva:

// Supongamos que la clase PersonV1 no tenía address
public class PersonV1 { public string Name; public int Age; }

// La nueva tiene address
public class PersonV2 { public string Name; public int Age; public string Address; }

// Migración:
string oldJson = File.ReadAllText("person.json");
PersonV1 oldPerson = JsonSerializer.Deserialize<PersonV1>(oldJson);

PersonV2 migrated = new PersonV2
{
    Name = oldPerson.Name,
    Age = oldPerson.Age,
    Address = "Unknown"
};

5. Casos complejos y errores inesperados

Invarianza de campo y propiedades required

En C# 11 y posteriores aparecieron las propiedades required. Ahora, si un campo está marcado como required, la deserialización puede fallar si ese campo no está en los datos:

public class Person
{
    public string Name { get; set; }
    [JsonPropertyName("Age")]
    public required int Age { get; set; }
    public string Address { get; set; }
}

Si en los datos antiguos falta la propiedad Age — aparecerá una excepción por discrepancia en la estructura.

Cambio de tipo: int string

// Antes:
public class Record { public int Count; }
// Ahora:
public class Record { public string Count; }

Si en los datos está "Count":42, la deserialización a string puede funcionar (conversión inteligente), pero en sentido inverso — puede lanzarse una excepción.

Eliminar la clase base

Si un objeto serializado tenía herencia y cambias la jerarquía — la deserialización de archivos antiguos puede fallar, a veces de forma "silenciosa", otras de forma evidente.

6. Errores típicos al trabajar con compatibilidad

Error Nº1: cambiar propiedades existentes sin pensar.
Renombrar o cambiar el tipo de propiedades sin considerar los datos ya serializados provoca pérdida de información al deserializar.

Error Nº2: olvidar nullable para campos nuevos.
Los campos nuevos deberían ser nullable o tener valores por defecto razonables.

Error Nº3: no probar la compatibilidad hacia atrás.
Cambiaste una clase — asegúrate de que los archivos/datos antiguos todavía se cargan correctamente.

Error Nº4: mezclar atributos de distintas librerías.
No uses JsonPropertyName y JsonProperty a la vez en la misma propiedad.

2
Tarea
C# SELF, nivel 45, lección 4
Bloqueada
Manejo de ausencia de campos durante la deserialización
Manejo de ausencia de campos durante la deserialización
1
Cuestionario/control
Configuración de la serialización, nivel 45, lección 4
No disponible
Configuración de la serialización
Configuración de la serialización de objetos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION