Start Debugging

Solución: System.Text.Json.JsonException: The JSON value could not be converted

System.Text.Json lanza esta excepción cuando el token JSON entrante no coincide con el tipo CLR de destino. Haz que el JSON coincida con el tipo, o registra un JsonConverter o una JsonSerializerOption que los reconcilie.

La solución: System.Text.Json lanzó esta excepción porque el token JSON en la ruta que indica no puede deserializarse en el tipo CLR de destino. El mensaje suele aparecer en dos formas. O bien el JSON tiene el tipo de token equivocado (una cadena donde se esperaba un número, o al revés, o un número donde se esperaba un valor de enum), o bien el JSON tiene el tipo correcto pero el contenido no coincide con las reglas de análisis del tipo (una fecha que no es ISO, un Guid mal formado, un literal como "NaN"). Elige una de las tres soluciones por sitio de llamada: cambia el productor para que emita la forma JSON esperada, activa un convertidor que acepte la forma que realmente recibes (JsonStringEnumConverter, JsonNumberHandling.AllowReadingFromString), o escribe un JsonConverter<T> que haga el análisis tú mismo.

System.Text.Json.JsonException: The JSON value could not be converted to MyApp.OrderStatus. Path: $.status | LineNumber: 0 | BytePositionInLine: 21.
   at System.Text.Json.ThrowHelper.ThrowJsonException(String message)
   at System.Text.Json.Serialization.Converters.EnumConverter`1.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)

Esta guía está escrita contra .NET 11 versión preliminar 4 y System.Text.Json 11.0.0-preview.4. El tipo de excepción y los campos Path / LineNumber / BytePositionInLine se han mantenido estables desde que System.Text.Json se publicó en .NET Core 3.0, así que cada solución a continuación se aplica a .NET 6, 8, 10 y 11 sin cambios. Lo único que se ha movido entre versiones son los ajustes del convertidor: JsonNumberHandling llegó en .NET 5, JsonStringEnumConverter<TEnum> (la versión genérica, compatible con AOT) se publicó en .NET 8, y el atributo [JsonStringEnumMemberName] aterrizó en .NET 9.

Lo primero que hay que leer es el segmento Path. Es una ruta al estilo JSON Pointer con $ como raíz, y te dice exactamente qué propiedad atragantó al lector. Los motores de búsqueda tienden a llevarte a una traza de pila que nombra un convertidor genérico (“the JSON value could not be converted to Int32”) sin pista de qué propiedad, pero Path siempre está ahí. Si no ves un segmento Path, estás viendo una excepción envuelta. Captura JsonException directamente en el sitio de llamada de la deserialización y lee ex.Path desde la propiedad, no desde ex.Message.

Por qué System.Text.Json se niega a coaccionar

A diferencia de Newtonsoft.Json, System.Text.Json es estricto por defecto: no coacciona entre tipos de tokens JSON y no ejecuta un analizador con pérdida sobre el contenido de la cadena. Si tu endpoint acepta { "amount": "12.50" } y tu DTO tiene decimal Amount, el lector ve JsonTokenType.String, no encuentra conversión incorporada de string a decimal y lanza. La misma lógica rechaza:

La estricticidad es el diseño, no el error. La justificación del equipo es que la conversión con pérdida es la fuente más común de corrupción silenciosa de datos en aplicaciones de negocio, y que el registro explícito de convertidores hace visible la intención en la frontera entre el formato de cable y el sistema de tipos. La contrapartida es que toda preocupación transversal que solías obtener de los valores por defecto de Newtonsoft.Json (cadena-a-número, cadena-a-enum, cadena-a-bool) ahora necesita una activación explícita.

Reproducción mínima

El programa más pequeño que reproduce la variante más común, un enum que llega como cadena:

// .NET 11 preview 4, System.Text.Json 11.0.0-preview.4
using System.Text.Json;

var json = """{ "status": "Pending" }""";

var order = JsonSerializer.Deserialize<Order>(json);

Console.WriteLine(order!.Status);

public record Order(OrderStatus Status);
public enum OrderStatus { Pending, Shipped, Delivered }

Ejecutar esto lanza:

Unhandled exception. System.Text.Json.JsonException:
The JSON value could not be converted to OrderStatus. Path: $.status | LineNumber: 0 | BytePositionInLine: 20.

El convertidor de enums por defecto acepta solo la representación entera ({ "status": 0 }). Cualquier otra cosa dispara la excepción. Una vez entiendes que este es el modo de fallo, todas las demás variantes en este post siguen el mismo patrón: la forma JSON no coincide con el tipo, y eliges qué lado doblar.

Solución 1: enums recibidos como cadenas

Registra JsonStringEnumConverter<TEnum> una vez en tu JsonSerializerOptions:

// .NET 11 preview 4, System.Text.Json 11.0.0-preview.4
var options = new JsonSerializerOptions
{
    Converters = { new JsonStringEnumConverter<OrderStatus>() }
};

var order = JsonSerializer.Deserialize<Order>(json, options)!;

Si tienes muchos enums, registra en su lugar el JsonStringEnumConverter() no genérico. Se aplica a cada tipo enum que el serializador encuentra, al costo de ser incompatible con el recorte de Native AOT. Para Native AOT, prefiere el convertidor genérico por enum o anota cada enum con [JsonConverter(typeof(JsonStringEnumConverter<OrderStatus>))] directamente, luego mantén la lista global de convertidores vacía.

Si el productor distingue entre mayúsculas y minúsculas (“PENDING” en el cable, Pending en C#), pasa una política de nombres:

var options = new JsonSerializerOptions
{
    Converters =
    {
        new JsonStringEnumConverter<OrderStatus>(JsonNamingPolicy.SnakeCaseUpper)
    }
};

Para productores que envían valores que no se pueden representar como identificadores de C# (“in-progress”, “n/a”), .NET 9 introdujo [JsonStringEnumMemberName]:

public enum OrderStatus
{
    Pending,

    [JsonStringEnumMemberName("in-progress")]
    InProgress,

    Delivered
}

En las APIs mínimas de ASP.NET Core, configura el convertidor en las JsonSerializerOptions de la aplicación para que cada endpoint lo recoja:

// .NET 11 preview 4, ASP.NET Core 11.0.0-preview.4
builder.Services.ConfigureHttpJsonOptions(o =>
{
    o.SerializerOptions.Converters.Add(new JsonStringEnumConverter<OrderStatus>());
});

Para controladores, la llamada equivalente es builder.Services.AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(...)). Configurarlo una vez en la raíz de composición es preferible a abrir JsonConverter en cada atributo [JsonConverter(...)].

Solución 2: números recibidos como cadenas (y viceversa)

Cuando el productor codifica en JSON un campo numérico como cadena, { "amount": "12.50" }, activa JsonNumberHandling.AllowReadingFromString:

// .NET 11 preview 4, System.Text.Json 11.0.0-preview.4
var options = new JsonSerializerOptions
{
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};

var dto = JsonSerializer.Deserialize<Invoice>(json, options);

public record Invoice(decimal Amount);

AllowReadingFromString funciona para todo tipo integral y de punto flotante, más decimal. Es simétrico con WriteAsString, que puedes combinar si quieres que el serializador emita y lea el valor como cadena:

NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString

Para un alcance por propiedad en lugar de global, anota la propiedad directamente:

public record Invoice(
    [property: JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] decimal Amount
);

El caso inverso, un número JSON enviado donde se espera un string (un ID de seguimiento codificado como 42 en lugar de "42"), no está cubierto por JsonNumberHandling. Necesitas un pequeño convertidor personalizado:

// .NET 11 preview 4
public sealed class NumberOrStringConverter : JsonConverter<string>
{
    public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => reader.TokenType switch
        {
            JsonTokenType.String => reader.GetString(),
            JsonTokenType.Number => reader.GetInt64().ToString(System.Globalization.CultureInfo.InvariantCulture),
            JsonTokenType.Null => null,
            _ => throw new JsonException()
        };

    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
        => writer.WriteStringValue(value);
}

Cubro el contrato del convertidor con más profundidad en la guía para escribir un JsonConverter personalizado, incluyendo cómo componer convertidores y cómo cortocircuitar el despacho polimórfico.

Solución 3: booleanos recibidos como 0 / 1 o como cadenas “true” / “false”

System.Text.Json solo acepta los tokens JSON true / false para bool. El numérico 0 / 1 y las cadenas "true" / "false" se rechazan ambos. No hay opción incorporada para ninguna de las dos formas; escribe un convertidor:

// .NET 11 preview 4
public sealed class LooseBooleanConverter : JsonConverter<bool>
{
    public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => reader.TokenType switch
        {
            JsonTokenType.True => true,
            JsonTokenType.False => false,
            JsonTokenType.Number => reader.GetInt32() != 0,
            JsonTokenType.String => bool.Parse(reader.GetString()!),
            _ => throw new JsonException()
        };

    public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        => writer.WriteBooleanValue(value);
}

Regístralo por propiedad con [JsonConverter(typeof(LooseBooleanConverter))] en lugar de globalmente, porque la mayoría de los campos en una API bien comportada no son flexibles, y doblar cada bool para aceptar tres formas oscurece el contrato.

Solución 4: cadenas vacías para tipos por valor

Si el productor emite { "shippedAt": "" } para una fecha ausente, System.Text.Json lee JsonTokenType.String y falla al analizar el contenido vacío como DateTime. Hay dos soluciones limpias. La primera es hacer la propiedad C# anulable y escribir un convertidor que mapee "" a null:

public sealed class NullableDateTimeConverter : JsonConverter<DateTime?>
{
    public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null) return null;
        if (reader.TokenType == JsonTokenType.String)
        {
            var s = reader.GetString();
            return string.IsNullOrEmpty(s) ? null : DateTime.Parse(s, System.Globalization.CultureInfo.InvariantCulture);
        }
        throw new JsonException();
    }

    public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
        => writer.WriteStringValue(value);
}

La segunda es establecer JsonSerializerOptions.RespectNullableAnnotations = true (por defecto en .NET 9+) y hacer que el productor envíe null en lugar de "". La solución del lado productor es preferible cuando controlas ambos lados, porque cada caso especial ""-como-null acaba filtrándose en una columna no anulable o una NullReferenceException río abajo.

Solución 5: Guid en formato incorrecto

System.Text.Json usa Guid.TryParseExact con formato D (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). Las representaciones con llaves ({...}), con paréntesis ((...)) y sin guiones (N) todas lanzan. La solución más corta es un convertidor por propiedad:

public sealed class FlexibleGuidConverter : JsonConverter<Guid>
{
    public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var s = reader.GetString();
        return Guid.Parse(s!);
    }

    public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
        => writer.WriteStringValue(value);
}

Guid.Parse acepta cada cadena de formato estándar, así que esto es suficiente.

Solución 6: objetos polimórficos con un discriminador desconocido

Cuando declaras un tipo base con [JsonPolymorphic] y [JsonDerivedType], el lector inspecciona la propiedad discriminadora y camina al subtipo correspondiente. Si el valor del discriminador falta o es desconocido, el lector lanza “the JSON value could not be converted to BaseType”. Dos soluciones de grado de producción:

// .NET 11 preview 4
[JsonPolymorphic(
    TypeDiscriminatorPropertyName = "$kind",
    UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonDerivedType(typeof(EmailEvent), "email")]
[JsonDerivedType(typeof(SmsEvent), "sms")]
public abstract record Event;

public record EmailEvent(string To, string Subject) : Event;
public record SmsEvent(string To, string Body) : Event;

El comportamiento por defecto (JsonUnknownDerivedTypeHandling.FailSerialization) es correcto para esquemas cerrados donde se conoce cada variante. Usa las opciones de fallback cuando ingieras eventos de un productor que añade variantes nuevas sin coordinar un despliegue.

Solución 7: NaN e Infinity de punto flotante

Si un serializador del lado productor emite "NaN", "Infinity" o "-Infinity" para double / float, actívalo:

var options = new JsonSerializerOptions
{
    NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
};

Esto es simétrico: la misma opción también permite a tu serializador emitir esos literales en la ruta de escritura. Sin él, tanto leer como escribir lanzan.

Trampas y casos parecidos

Relacionados

Fuentes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Volver