Serialización de tipos de unión con System.Text.Json

A partir de .NET 11, JsonSerializer admite tipos de unión de C# 15. Una unión contiene uno de los tipos de caso en su declaración. JsonSerializer escribe el valor de caso activo y puede leerlo de nuevo.

Serializar y deserializar valores de unión

Declare una unión cuyos casos tengan tipos de token JSON distintos:

public union Payload(int, string, Message);
public sealed record Message(string Text);

Utilice JsonSerializer para serializar y deserializar la unión:

Payload payload = new Message("Ready");
string json = JsonSerializer.Serialize(payload);
Payload copy = JsonSerializer.Deserialize<Payload>(json);

El JSON serializado contiene el valor de caso activo en lugar de un contenedor o discriminador:

{"Text":"Ready"}

De forma predeterminada, el serializador clasifica JSON entrante por tipo de token. En la unión anterior, un número JSON selecciona int, una cadena JSON selecciona stringy un objeto JSON selecciona Message.

Con JsonSerializerOptions.Web, int también se puede leer desde una cadena JSON. Tanto los casos int como string de Payload reclaman entonces el tipo de token de cadena. Incluso "25%" lanza JsonException por ambigüedad antes de que el serializador analice ninguno de los dos casos. Para leer cadenas con valores predeterminados web, proporcione un clasificador personalizado que elija el caso.

Distinción de casos con el mismo tipo de token JSON

La clasificación de tokens no puede distinguir dos casos que se serializan como objetos JSON. Aplique JsonUnionAttribute y seleccione JsonUnionTypeStructuralClassifier para clasificar los casos de objeto por sus nombres de propiedad:

[JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))]
public union Pet(Dog, Cat);
public sealed record Dog(string Name, string Breed);
public sealed record Cat(string Name, int Lives);

Aquí, JsonUnionAttribute selecciona un clasificador para la unión existente Pet ; aplicarlo a un tipo normal no convierte ese tipo en una unión.

El clasificador selecciona Dog cuando la carga contiene Breed y selecciona Cat cuando contiene Lives:

Pet pet = JsonSerializer.Deserialize<Pet>(
    """{"Name":"Rex","Breed":"Husky"}""");

En el caso de los objetos JSON, el clasificador estructural empieza con los tipos de objeto compatibles y va reduciendo ese conjunto a medida que lee los nombres de propiedades reconocidos del nivel raíz. Las propiedades obligatorias eliminan los casos cuando estos están ausentes. JsonUnmappedMemberHandling.Disallow elimina un caso si el payload contiene una propiedad que no está declarada en el caso. La clasificación solo se realiza correctamente cuando permanece un caso.

El clasificador estructural no inspecciona los valores de propiedad, los objetos anidados, el contenido de cadenas ni los elementos de matriz. Tenga en cuenta estas consecuencias:

  • Una carga que deja cero o varios candidatos produce JsonException.
  • Los contratos de objetos superpuestos o sombreados pueden rechazarse cuando se crea el clasificador.
  • No se admiten varios casos que no son objetos que usan el mismo tipo de token JSON. Por ejemplo, Guid y string ambos usan cadenas JSON.
  • Un caso de objeto simple no se puede mezclar con un diccionario, JsonObject, ni con otro caso con forma de objeto que no sea POCO.
  • No se admiten casos de unión anidados ni casos polimórficos.
  • ReferenceHandler.Preserve no es compatible.
  • Una configuración que no puede distinguir sus casos produce NotSupportedException cuando el serializador compila el clasificador.

Elegir uniones o jerarquías cerradas

Use una unión cuando necesite conservar un formato JSON sin discriminadores que no controle o cuando los casos tengan formas JSON distintas. Por ejemplo, los int casos y string de Payload se distinguen por tipo de token JSON. En los casos de objeto como Pet(Dog, Cat), sin embargo, los cambios en los nombres de propiedad pueden afectar a qué caso selecciona un clasificador estructural.

Al controlar los tipos y el contrato JSON, una jerarquía cerrada con polimorfismo inferido puede identificar casos de objeto con un discriminador en su lugar:

[JsonPolymorphic(InferClosedTypePolymorphism = true)]
public closed record Event;
public sealed record Created(int Id) : Event;
public sealed record Deleted(int Id) : Event;

JsonSerializer.Serialize<Event>(new Created(42)) escribe {"$type":"Created","Id":42}. Ambos tipos derivados declaran Id, pero el discriminador identifica el caso independientemente de sus propiedades. Esto hace que la selección de casos sea más estable a medida que evolucionan las propiedades. A diferencia de los casos de unión, los tipos derivados deben compartir una clase base, y hay que optar explícitamente por el polimorfismo inferido; el modificador closed por sí solo no añade un discriminador.

Proporcionar un clasificador personalizado

Deriva de JsonTypeClassifierFactory cuando la clasificación predeterminada de tokens o el JsonUnionTypeStructuralClassifier integrado no satisfagan tus requisitos. Un clasificador personalizado puede usar otras reglas estructurales para seleccionar un caso de unión. Registre la fábrica en una de estas ubicaciones:

Un clasificador lee el valor JSON actual y devuelve uno de los tipos de caso de JsonTypeClassifierContext.UnionCases. El serializador comprueba primero el delegado del contrato, a continuación la factoría para cada unión, las factorías definidas en las opciones y la clasificación integrada de tokens.

Para uniones ambiguas, el generador de origen notifica un diagnóstico a menos que se configure un clasificador en tiempo de generación.

Controlar valores nulos y valores de unión predeterminados

Una unión puede declarar tipos de caso anulables. JSON null selecciona el primer caso anulable. Si la unión no tiene ningún caso anulable, JSON null produce el valor predeterminado de la unión. Para una unión de estructura generada por el compilador, el valor predeterminado no tiene ningún caso activo y serializa como JSON null.

Personalización de un contrato de unión

Para escenarios avanzados, personalice los metadatos de unión mediante JsonTypeInfo. El valor de la unión JsonTypeInfo.Kind es JsonTypeInfoKind.Union. Su contrato expone:

Para obtener más información sobre cómo modificar JsonTypeInfo, consulte Personalizar un contrato JSON.

Consulte también