Runtime: ¿Soporte de JsonSerializer para TimeSpan en 3.0?

Creado en 18 jun. 2019  ·  36Comentarios  ·  Fuente: dotnet/runtime

_Disculpas si esto ya está respondido en otro lugar, pero si fueron mis habilidades de búsqueda fallaron ._

Estoy probando la versión preliminar 6 de 3.0 con algún código de aplicación de producción existente, y uno de los problemas que surgieron de las pruebas es que algunos de nuestros objetos existentes que usamos en nuestros contratos de API usan propiedades que son TimeSpan valores, que luego se representan como cadenas.

¿Está previsto el soporte para las propiedades TimeSpan para 3.0 para las nuevas API System.Text.Json?

Si no va a ser, eso nos avisa para hacer una refactorización antes de septiembre para cambiarlos a cadenas para que podamos usar el nuevo serializador, donde, como si estuviera planeado pero aún no implementado, solo tenemos que esperar una vista previa posterior. para que esto funcione.

A continuación se muestra una prueba de unidad de reproducción mínima que demuestra la falla de TimeSpan para ser manejado en comparación con nuestro código JSON .NET existente.

using System;
using System.Text.Json.Serialization;
using Xunit;
using JsonConvert = Newtonsoft.Json.JsonConvert;
using JsonSerializer = System.Text.Json.Serialization.JsonSerializer;

namespace JsonSerializerTimeSpanNotSupportedException
{
    public static class Repro
    {
        [Fact]
        public static void Can_Deserialize_Object_With_SystemTextJson()
        {
            // Arrange
            string json = "{\"child\":{\"value\":\"00:10:00\"}}";

            // Act (fails in preview 6, throws: System.Text.Json.JsonException : The JSON value could not be converted to System.TimeSpan. Path: $.child.value | LineNumber: 0 | BytePositionInLine: 28.)
            var actual = JsonSerializer.Parse<Parent>(json, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });

            // Assert
            Assert.NotNull(actual);
            Assert.NotNull(actual.Child);
            Assert.Equal(TimeSpan.FromMinutes(10), actual.Child.Value);
        }

        [Fact]
        public static void Can_Deserialize_Object_With_NewtonsoftJson()
        {
            // Arrange
            string json = "{\"child\":{\"value\":\"00:10:00\"}}";

            var actual = JsonConvert.DeserializeObject<Parent>(json);

            // Assert
            Assert.NotNull(actual);
            Assert.NotNull(actual.Child);
            Assert.Equal(TimeSpan.FromMinutes(10), actual.Child.Value);
        }

        private sealed class Parent
        {
            public Child Child { get; set; }
        }

        private sealed class Child
        {
            public TimeSpan Value { get; set; }
        }
    }
}
api-suggestion area-System.Text.Json json-functionality-doc

Comentario más útil

  • No está claro si existe una codificación estándar para lapsos de tiempo.

La codificación estándar para intervalos de tiempo sería XML usa para representar TimeSpan y ya está implementado en XmlConvert y XsdDuration :

https://github.com/dotnet/corefx/blob/6d723b8e5ae3129c0a94252292322fc19673478f/src/System.Private.Xml/src/System/Xml/XmlConvert.cs#L1128 -L1146

https://github.com/dotnet/corefx/blob/6d723b8e5ae3129c0a94252292322fc19673478f/src/System.Private.Xml/src/System/Xml/Schema/XsdDuration.cs#L229 -L236

Ahora, posiblemente, TimeSpan sí mismo debería admitir el análisis y el formateo de formatos de duración ISO8601. ¿Quizás sería mejor, como primer paso, agregar una nueva cadena de formato estándar TimeSpan , similar al especificador de formato de ida y vuelta ("O", "o") de DateTime[Offset] ? Agregar un convertidor después de eso sería realmente simple.

Todos 36 comentarios

Actualmente no tenemos planes de admitir TimeSpan y se agregarían en el futuro, pero puedo investigar un poco para ver cuánto trabajo implica. Alternativamente, puede crear su propio JsonConverter para admitir TimeSpan como solución alternativa. Daré una actualización a finales de la próxima semana. Gracias.

Si tuviéramos que agregarlo, también querríamos agregar TimeSpan API al lector / escritor / JsonElement, que tendría que pasar por una revisión de api.

Gracias, un convertidor personalizado también sería una forma bastante fácil de hacerlo funcionar para nuestra aplicación para 3.0. ¿Está previsto que esas capacidades se envíen con la versión preliminar 7?

Sí, la compatibilidad con el convertidor personalizado ahora está en el maestro y, por lo tanto, estará en la vista previa 7.

Sin embargo, dado que TimeSpan es un tipo de BCL común, deberíamos proporcionar un convertidor predeterminado.

Revisamos esto hoy:

  • Hay un argumento a favor, porque es un tipo de valor. En principio, podríamos optimizar el análisis para que sea libre de asignaciones (es decir, evitar pasar por una cadena).
  • No está claro si existe una codificación estándar para lapsos de tiempo.
  • Por ahora, cierre. Esto se puede volver a abrir si hay suficiente evidencia del cliente de que es necesario.
  • No está claro si existe una codificación estándar para lapsos de tiempo.

La codificación estándar para intervalos de tiempo sería XML usa para representar TimeSpan y ya está implementado en XmlConvert y XsdDuration :

https://github.com/dotnet/corefx/blob/6d723b8e5ae3129c0a94252292322fc19673478f/src/System.Private.Xml/src/System/Xml/XmlConvert.cs#L1128 -L1146

https://github.com/dotnet/corefx/blob/6d723b8e5ae3129c0a94252292322fc19673478f/src/System.Private.Xml/src/System/Xml/Schema/XsdDuration.cs#L229 -L236

Ahora, posiblemente, TimeSpan sí mismo debería admitir el análisis y el formateo de formatos de duración ISO8601. ¿Quizás sería mejor, como primer paso, agregar una nueva cadena de formato estándar TimeSpan , similar al especificador de formato de ida y vuelta ("O", "o") de DateTime[Offset] ? Agregar un convertidor después de eso sería realmente simple.

Esto también cambió el comportamiento predeterminado de cómo AspNetCore entrega el tipo de devolución TimeSpan en la API, consulte dotnet / AspnetCore # 11724. Este podría ser un cambio rotundo.

La solución más simple es crear un convertidor personalizado:

public class TimeSpanConverter : JsonConverter<TimeSpan>
    {
        public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return TimeSpan.Parse(reader.GetString());
        }

        public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString());
        }
    }

Hablando de la evidencia del cliente: para nuestro desarrollo de software, el soporte para TimeSpan es muy necesario.

Me encontré con esto hoy al portar una aplicación a .NET Core 3.0 también. Debido a que esto está cerrado, ¿eso significa que Microsoft no tiene planes de agregar soporte nativo? El comentario de @khellang me parece un argumento bastante convincente de que debería estar en una hoja de ruta en alguna parte ...

Reapertura para 5.0 basado en preguntas adicionales. La duración de ISO8601 es probablemente la mejor representación, aunque también se debe considerar la compatibilidad con Newtonsoft.

Me encontré con este problema hoy. El comportamiento predeterminado es peor que el comportamiento anterior y fue completamente inesperado. Deberíamos arreglarlo, ya sea usando ISO8601 o siendo compatible con Newtonsoft.

El comportamiento predeterminado es peor que el comportamiento anterior y fue completamente inesperado.

@mfeingol ¿Qué comportamiento? ¿Que simplemente falla?

Deberíamos arreglarlo, ya sea usando ISO8601 o siendo compatible con Newtonsoft.

Es realmente fácil agregar la solución alternativa que @rezabayesteh mencionó.

@khellang : lo que observé con un proyecto ASP.NET Core relativamente básico es que serializa un campo Timespan? como un campo HasValue , y luego cada una de las propiedades de la estructura TimeSpan.

Es realmente fácil agregar la solución alternativa

Lo hice, pero eso no debería ser necesario para un tipo de uso tan común.

Acabo de encontrar este problema hoy (informado por mi cliente) y tengo que volver a cambiar todas mis aplicaciones y webapi aspnet para usar el serializador Newtonsoft.Json en lugar de usar la configuración en Startup.cs:

services.AddControllers ()
.AddNewtonsoftJson (opciones =>
{
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});

En mis casos, uso algunos TimeSpan que aceptan valores NULL (¿TimeSpan?) Y System.Text.Json lo ha serializado como:

{
"hasValue": verdadero,
"valor": {
"tick": 0,
"días": 0,
"horas": 0,
"milisegundos": 0,
"minutos": 0,
"segundos": 0,
"totalDays": 0,
"totalHours": 0,
"totalMilliseconds": 0,
"totalMinutes": 0,
"totalSeconds": 0
}
}

Esto causa un pequeño problema para los objetos javascript en los navegadores web, así como para varios deserializadores multiplataforma (es decir, varios lenguajes de programación) que consumen mis apis.

Esperaría el mismo resultado de serialización que el serializador Newtonsoft.Json:

{
"timeSpan": "00: 00: 00.0000000",
"nullTimeSpan": nulo
}

Acabo de encontrar este problema hoy (informado por mi cliente) y tengo que volver a cambiar todas mis aplicaciones y webapi aspnet para usar el serializador Newtonsoft.Json en su lugar

@bashocz ¿ https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476 no funciona para usted?

¿Por qué la solución alternativa mencionada en # 38641 (comentario) no funciona para usted?

@khellang Solo quiero resaltar que la serialización de TimeSpan es un problema para otros desarrolladores y prestarle atención. Me gustaría mucho cambiar a System.Text.Json, sin embargo, hay algunos obstáculos.

Investigué el nuevo System.Text.Json hace unas semanas y descubrí que no está completo. He planteado un problema dotnet / corefx # 41747 y se señaló a otros dotnet / corefx # 39031, dotnet / corefx # 41325, dotnet / corefx # 38650 relacionados. Por eso, todos nuestros microservicios internos todavía usan Newtonsoft.Json.

Por una razón desconocida, me olvidé de administrar los desarrolladores para solucionarlo también en las API públicas y en las aplicaciones web.

Por cierto: trato de evitar las soluciones alternativas tanto como sea posible en el código de producción ... es difícil mantenerlo y eliminarlo en el futuro.

@khellang , no es que una solución alternativa no funcione. Es algo tan básico que no debería necesitar que los desarrolladores agreguen una solución alternativa. Como una gran característica introducida para .NET core 3, no debería carecer de implementaciones tan básicas.

@arisewanggithub Hay configuraciones globales disponibles para los controladores. Se puede configurar a través de AddJsonOptions() , o puede pasar un JsonSerializerOptions instancia a la Json() método controlador.

¿Está cerrado porque tenemos una solución?

¿Está cerrado porque tenemos una solución?

El problema aún está abierto, pendiente de una propuesta, revisión e implementación de API / comportamiento. Mientras tanto, es bastante fácil solucionarlo utilizando el convertidor mencionado en https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476.

Para cualquiera que esté bloqueado por esto, aquí hay un paquete NuGet con un convertidor (JsonTimeSpanConverter) que podemos usar antes de la versión 5.0:

Admite los tipos TimeSpan y Nullable <TimeSpan>.

¡Oh, mierda, esto duele!

Para cualquiera que esté bloqueado por esto, aquí hay un paquete NuGet con un convertidor (JsonTimeSpanConverter) que podemos usar antes de la versión 5.0:

Soporta TimeSpan y Nullabletipos.

Según mis pruebas, el marco ya maneja los tipos de valores que aceptan valores NULL. No necesitas mucho más que lo siguiente:

public class DelegatedStringJsonConverter<T> : JsonConverter<T>
    where T : notnull
{
    private static readonly bool s_typeAllowsNull = !typeof(T).IsValueType || Nullable.GetUnderlyingType(typeof(T)) != null;

    private readonly Func<string, T> _parse;
    private readonly Func<T, string> _toString;

    public DelegatedStringJsonConverter(Func<string, T> parse, Func<T, string> toString)
    {
        _parse = parse;
        _toString = toString;
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // null tokens are handled by the framework except when the expected type is a non-nullable value type
        // https://github.com/dotnet/corefx/blob/v3.1.4/src/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleNull.cs#L58
        if (!s_typeAllowsNull && reader.TokenType == JsonTokenType.Null)
            throw new JsonException($"{typeof(T)} does not accept null values.");

        return _parse(reader.GetString());
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        // value is presumably not null here as null values are handled by the framework
        writer.WriteStringValue(_toString(value));
    }
}

(EDITAR: después de observar más de cerca las fuentes del marco, resultó que las comprobaciones de tokens nulos / valores nulos son redundantes. El código anterior se actualizó en consecuencia).

Luego configura JsonSerializerOptions así:

options.Converters.Add(new DelegatedStringJsonConverter<TimeSpan>(
    value => TimeSpan.Parse(value, CultureInfo.InvariantCulture),
    value => value.ToString(null, CultureInfo.InvariantCulture)));

No creo que sea una buena idea agregar una dependencia de terceros si puede hacer frente a unas pocas líneas de código adicional. Después de todo, no estamos en la tierra de la NPM. ;)

Por supuesto, sería mejor si no necesitáramos una solución alternativa para un tipo tan básico.

@ adams85 FYI Los tipos de valor que aceptan valores NULL tienen errores <.NET 5 cuando se usan con JsonConverterAttribute. Ver # 32006. Personalmente, prefiero usar el estilo de atributo sobre la configuración global, de ahí las extensiones y la corrección de errores, pero su convertidor es excelente. ¿Le importa si lo agrego a Macross.Json.Extensions para ayudar a cualquier otra persona que también pueda beneficiarse de él y no le importe viajar a "NPM land" de vez en cuando? :)

@CodeBlanch Gracias por señalar esta peculiaridad con JsonConverterAttribute . Definitivamente es parte de la historia completa.

Y, por supuesto, puede agregar mi convertidor a su lib si lo desea. :)

Comenzando con .NET 5.0 Preview 5, he estado recibiendo Cannot skip tokens on partial JSON errores al transferir mis entidades a JSON con System.Text.Json. La línea y el carácter ofensivos son los dos puntos en "Ticks":

De todos modos, si utilizo la solución alternativa y serializo TimeSpan de manera sensata, funciona bien.

+1 de mí porque mi reacción inicial al abrir el archivo .json para inspeccionarlo solo para encontrar el TimeSpan serializado en toda su gloria estructural fue "seguramente no ..."

De @jsedlak en https://github.com/dotnet/runtime/issues/42356 :

Título del problema

La clase System.Text.Json.JsonSerializer no puede deserializar una propiedad TimeSpan, aunque puede serializarla.

General

El proyecto de muestra que demuestra este problema está disponible aquí: https://github.com/jsedlak/TestTimeSpan

Si devuelve una propiedad TimeSpan en un WebApi, se serializa correctamente en JSON:

[ { "forecastLength": { "ticks": 36000000000, "days": 0, "hours": 1, "milliseconds": 0, "minutes": 0, "seconds": 0, "totalDays": 0.041666666666666664, "totalHours": 1, "totalMilliseconds": 3600000, "totalMinutes": 60, "totalSeconds": 3600 } } ]

Pero las extensiones de deserializador predeterminadas ( HttpClient.GetFromJsonAsync ) no pueden manejar la propiedad. Devuelve un TimeSpan vacío. Se debe utilizar un convertidor personalizado para deserializar el objeto.

Información de DotNet

`.NET Core SDK (que refleja cualquier archivo global.json):
Versión: 3.1.302
Confirmar: 41faccf259

Entorno de ejecución:
Nombre del sistema operativo: Windows
Versión del SO: 10.0.20201
Plataforma del sistema operativo: Windows
RID: win10-x64
Ruta base: C: \ Archivos de programa \ dotnet \ sdk3.1.302 \

Anfitrión (útil para soporte):
Versión: 3.1.6
Confirmar: 3acd9b0cd1

SDK de .NET Core instalados:
3.1.302 [C: \ Archivos de programa \ dotnet \ sdk]

Tiempos de ejecución de .NET Core instalados:
Microsoft.AspNetCore.All 2.1.20 [C: \ Archivos de programa \ dotnet \ shared \ Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.20 [C: \ Archivos de programa \ dotnet \ shared \ Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.6 [C: \ Archivos de programa \ dotnet \ shared \ Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.20 [C: \ Archivos de programa \ dotnet \ shared \ Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.6 [C: \ Archivos de programa \ dotnet \ shared \ Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.6 [C: \ Archivos de programa \ dotnet \ shared \ Microsoft.WindowsDesktop.App] `

Nos gustaría abordar esto en .NET 6.0 y apreciaríamos una contribución de la comunidad aquí. Mientras tanto, la solución es utilizar un convertidor personalizado, por ejemplo, https://github.com/dotnet/runtime/issues/29932#issuecomment -540200476. La implementación debe basarse en el formato ISO8601 para que la duración sea coherente con el comportamiento de DateTime y DateTimeOffset .

Nos gustaría abordar esto en .NET 6.0

.NET 5 aún no se ha lanzado, me encantaría que mordiéramos la bala en v5 (semver permite cambios importantes v3 => v5) en lugar de esperar a v6.

No pudimos encajar esta función en 5.0. Tuvimos que priorizar junto con el resto del trabajo de la

Solo para resaltar la solución simple con un convertidor personalizado nuevamente:

public class TimeSpanConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return TimeSpan.Parse(reader.GetString());
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }
}

var options = new JsonSerializerOptions { Converters = { new TimeSpanConverter() };
JsonSerializer.Serialize(myObj, options);

...

@layomia , puedo tomar esto por 6.0.

Recomendaría usar la cultura invariante al optar por construir un convertidor json usted mismo:

public class TimeSpanConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return TimeSpan.Parse(reader.GetString(), CultureInfo.InvariantCulture);
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString(format: null, CultureInfo.InvariantCulture));
    }
}

Estoy trabajando con asp.net core 3.1 y encontré este problema hoy. Estoy usando la cuadrícula teleriks con señalizador y edición en línea en filas que contienen intervalos de tiempo que aceptan valores NULL y puedo ver que la carga útil contiene los datos de intervalos de tiempo, pero la deserialización falla. Terminé usando la biblioteca Macross.Json.Extensions y reelaborando parte de mi javascript para que funcione, aunque no es ideal, espero que esto tenga una solución adecuada.

Y si está buscando el formato ISO8601, puede usar esto:
`` c #

public class TimeSpanConverter : JsonConverter<TimeSpan>
{
    public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var stringValue = reader.GetString();
        return System.Xml.XmlConvert.ToTimeSpan(stringValue); //8601
    }

    public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options)
    {
        var stringValue = System.Xml.XmlConvert.ToString(value); //8601
        writer.WriteStringValue(stringValue);
    }
}

''

Considere TimeSpan de al menos 24 horas .. por ejemplo, "24:00:00" debe ser equivalente a new TimeSpan(24, 0, 0)

@gojanpaolo

Considere TimeSpan de al menos 24 horas. Por ejemplo, "24:00:00" debería ser equivalente al nuevo TimeSpan (24, 0, 0)

Eso no es realmente una cosa JSON, es parte de la lógica de análisis de TimeSpan. Respuesta corta: utilice "1.00: 00: 00" durante 24 horas. Respuesta larga: Hace un tiempo estuve revisando el código de tiempo de ejecución para descubrir por qué "24:00:00" no se analiza como se esperaba y escribí una publicación de blog al respecto.

¿Fue útil esta página
0 / 5 - 0 calificaciones