Runtime: Suporte JsonSerializer para TimeSpan em 3.0?

Criado em 18 jun. 2019  ·  36Comentários  ·  Fonte: dotnet/runtime

_Desculpe se isso já foi respondido em outro lugar, mas se foi minha capacidade de pesquisa me falhou._

Estou testando o preview 6 de 3.0 com algum código de aplicativo de produção existente, e um dos problemas que surgiram dos testes é que alguns de nossos objetos existentes que usamos em nossos contratos de API usam propriedades que são TimeSpan valores, que são então representados como strings.

O suporte para propriedades TimeSpan planejado para 3.0 para as novas APIs System.Text.Json?

Se não vai ser, isso nos avisa para fazer alguma refatoração antes de setembro para alterá-los para strings para que possamos usar o novo serializador, onde como se estivesse planejado, mas apenas não implementado ainda, então precisamos apenas esperar por uma visualização posterior para fazer isso funcionar.

Abaixo está um teste de unidade de reprodução mínimo que demonstra a falha de TimeSpan em comparação com nosso 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

Comentários muito úteis

  • Não está claro se existe uma codificação padrão para intervalos de tempo.

A codificação padrão para intervalos de tempo seria a "duração" do ISO8601 . Isso também é o que o XML usa para representar TimeSpan e já está implementado em XmlConvert e 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

Agora, indiscutivelmente, o próprio TimeSpan deve suportar a análise e a formatação dos formatos de duração ISO8601. Talvez seja melhor, como primeiro passo, adicionar uma nova string de formato TimeSpan padrão, semelhante ao especificador de formato de round-trip ("O", "o") de DateTime[Offset] ? Adicionar um conversor depois disso seria muito simples.

Todos 36 comentários

No momento, não temos planos de apoiar TimeSpan e seríamos adicionados no futuro, mas posso fazer algumas investigações para ver quanto trabalho está envolvido. Como alternativa, você pode criar seu próprio JsonConverter para oferecer suporte a TimeSpan como uma solução alternativa. Vou dar uma atualização no final da próxima semana. Obrigado.

Se fôssemos adicioná-lo, também quereríamos adicionar TimeSpan APIs ao leitor / escritor / JsonElement, que teria que passar pela revisão da API.

Obrigado - um conversor personalizado também seria uma maneira fácil de fazê-lo funcionar em nosso aplicativo para 3.0. Esses recursos estão planejados para serem enviados com o preview 7?

Sim, o suporte do conversor personalizado está agora no mestre e, portanto, estará na visualização 7.

No entanto, como TimeSpan é um tipo de BCL comum, ainda devemos fornecer um conversor padrão.

Nós revisamos isso hoje:

  • Há um argumento a favor disso, porque é um tipo de valor. Em princípio, poderíamos otimizar a análise para ficar livre de alocação (ou seja, evitar passar por uma string).
  • Não está claro se existe uma codificação padrão para intervalos de tempo.
  • Por enquanto, feche. Isso pode ser reaberto se houver evidência de cliente suficiente de que isso é necessário.
  • Não está claro se existe uma codificação padrão para intervalos de tempo.

A codificação padrão para intervalos de tempo seria a "duração" do ISO8601 . Isso também é o que o XML usa para representar TimeSpan e já está implementado em XmlConvert e 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

Agora, indiscutivelmente, o próprio TimeSpan deve suportar a análise e a formatação dos formatos de duração ISO8601. Talvez seja melhor, como primeiro passo, adicionar uma nova string de formato TimeSpan padrão, semelhante ao especificador de formato de round-trip ("O", "o") de DateTime[Offset] ? Adicionar um conversor depois disso seria muito simples.

Isso também mudou o comportamento padrão de como o AspNetCore retorna o tipo TimeSpan na API, consulte dotnet / AspnetCore # 11724. Esta pode ser uma mudança significativa.

A solução mais simples é criar um conversor 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());
        }
    }

Falando sobre a evidência do cliente: para o nosso suporte de desenvolvimento de software para TimeSpan é altamente necessário.

Corri para isso hoje ao portar um aplicativo para .NET Core 3.0 também. Como está fechado, isso significa que a Microsoft não tem planos de adicionar suporte nativo? O comentário de @khellang parece um argumento bastante convincente para mim de que deveria estar em um roteiro em algum lugar ...

Reabrindo para 5.0 com base em perguntas adicionais. A duração ISO8601 é provavelmente a melhor representação, embora a compatibilidade com Newtonsoft também deva ser considerada.

Acabei de encontrar esse problema hoje. O comportamento padrão é pior do que o comportamento anterior e foi totalmente inesperado. Devemos corrigi-lo, usando ISO8601 ou sendo compatível com Newtonsoft.

O comportamento padrão é pior do que o comportamento anterior e foi totalmente inesperado.

@mfeingol Qual comportamento? Que simplesmente falha?

Devemos corrigi-lo, usando ISO8601 ou sendo compatível com Newtonsoft.

É realmente fácil apenas adicionar a solução alternativa que @rezabayesteh mencionou.

@khellang : o que observei com um projeto ASP.NET Core relativamente básico é que ele serializa um Timespan? como um campo HasValue e, em seguida, cada uma das propriedades da estrutura TimeSpan.

É muito fácil apenas adicionar a solução alternativa

Sim, mas isso não deve ser necessário para um tipo tão comumente usado.

Acabei de encontrar esse problema hoje (relatado por meu cliente) e tenho que voltar todos os meus aplicativos e webapi aspnet para usar o serializador Newtonsoft.Json em vez de usar a configuração em Startup.cs:

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

Em meus casos, eu uso alguns TimeSpan (TimeSpan?) Anuláveis ​​e System.Text.Json os serializou como:

{
"hasValue": verdadeiro,
"valor": {
"tick": 0,
"dias": 0,
"horas": 0,
"milissegundos": 0,
"minutos": 0,
"segundos": 0,
"totalDays": 0,
"totalHours": 0,
"totalMilliseconds": 0,
"totalMinutes": 0,
"totalSeconds": 0
}
}

Isso causa um pequeno problema para objetos javascript em navegadores da web, bem como para vários desserializadores de plataforma cruzada (significa várias linguagens de programação) que consomem meu apis.

Eu esperaria o mesmo resultado de serialização do serializador Newtonsoft.Json:

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

Acabei de encontrar esse problema hoje (relatado por meu cliente) e tenho que voltar todos os meus aplicativos e webapi aspnet para usar o serializador Newtonsoft.Json.

@bashocz Por que a solução alternativa mencionada em https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476 funciona para você?

Por que a solução alternativa mencionada em # 38641 (comentário) não funciona para você?

@khellang, quero apenas destacar que a serialização do TimeSpan é um problema para outros desenvolvedores e dar atenção a isso. Eu gostaria muito de mudar para System.Text.Json, no entanto, existem alguns obstáculos.

Eu investiguei o novo System.Text.Json algumas semanas atrás e descobri que não é um recurso completo. Eu levantei um problema dotnet / corefx # 41747 e fui apontado para outro dotnet / corefx # 39031, dotnet / corefx # 41325, dotnet / corefx # 38650 relacionado. Por causa disso, todos os nossos microsserviços internos ainda usam Newtonsoft.Json.

Por motivo desconhecido, esqueci de gerenciar os desenvolvedores para corrigi-lo em APIs públicas e aplicativos da web também.

BTW: Eu tento evitar soluções alternativas tanto quanto possível no código de produção .. é difícil mantê-lo e removê-lo no futuro.

@khellang , não é que uma solução alternativa não funcione. É uma coisa tão básica que não deve precisar que os desenvolvedores adicionem uma solução alternativa. Como um grande recurso introduzido no .NET core 3, não deve faltar essas implementações básicas.

@arisewanggithub Existem configurações globais disponíveis para os controladores. Você pode configurá-lo por meio de AddJsonOptions() , ou pode passar uma instância JsonSerializerOptions para o método do controlador Json() .

Esta é uma causa encerrada que estamos tendo uma solução alternativa ?!

Esta é uma causa encerrada que estamos tendo uma solução alternativa ?!

O problema ainda está aberto, aguardando uma proposta, revisão e implementação de API / comportamento. Enquanto isso, é muito fácil contornar o problema usando o conversor mencionado em https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476.

Para qualquer pessoa bloqueada por isso, aqui está um pacote NuGet com um conversor (JsonTimeSpanConverter) que podemos usar antes da versão 5.0:

Suporta os tipos TimeSpan e Nullable <TimeSpan>.

Oh, merda, isso dói!

Para qualquer pessoa bloqueada por isso, aqui está um pacote NuGet com um conversor (JsonTimeSpanConverter) que podemos usar antes da versão 5.0:

Suporta TimeSpan e Nullabletipos.

De acordo com meus testes, os tipos de valor anulável já são tratados pela estrutura. Você não precisa de muito mais do que o seguinte:

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: depois de dar uma olhada mais de perto nas fontes do framework, descobriu-se que as verificações de tokens nulos / valores nulos são redundantes. O código acima foi atualizado de acordo.)

Em seguida, configure JsonSerializerOptions assim:

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

Não acho uma boa ideia adicionar uma dependência de terceiros se você puder lidar com essas poucas linhas de código extra. Afinal, não estamos nas terras da NPM. ;)

Claro, seria melhor se não precisássemos de uma solução alternativa para esse tipo básico.

@ adams85 FYI Tipos de valor anuláveis ​​são bugados <.NET 5 quando usados ​​com JsonConverterAttribute. Consulte # 32006. Eu pessoalmente prefiro usar o estilo de atributo em vez da configuração global, daí as extensões e correção de bug, mas seu conversor é excelente. Você se importa se eu adicioná-lo ao Macross.Json.Extensions para ajudar qualquer pessoa que também possa se beneficiar com ele e não se importe de viajar para "terras NPM" de vez em quando? :)

@CodeBlanch Obrigado por apontar esta peculiaridade com JsonConverterAttribute . Definitivamente, é parte da história completa.

E, claro, você pode adicionar meu conversor à sua biblioteca, se quiser. :)

Começando com .NET 5.0 Preview 5, tenho recebido Cannot skip tokens on partial JSON erros de ida e volta em minhas entidades para JSON com System.Text.Json. A linha e o caractere ofensivos são os dois pontos em "Ticks":

De qualquer forma, se eu usar a solução alternativa e serializar TimeSpan de uma forma sensata, ele funciona bem.

+1 de mim porque minha reação inicial ao abrir o arquivo .json para inspecioná-lo apenas para encontrar o TimeSpan serializado em toda a sua glória estrutural foi "certamente não ..."

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

Título do problema

A classe System.Text.Json.JsonSerializer não consegue desserializar uma propriedade TimeSpan, embora possa serializá-la.

Em geral

Um exemplo de projeto que demonstra esse problema está disponível aqui: https://github.com/jsedlak/TestTimeSpan

Se você retornar uma propriedade TimeSpan em um WebApi, ela será serializada corretamente para 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 } } ]

Mas as extensões desserializador padrão ( HttpClient.GetFromJsonAsync ) não podem lidar com a propriedade. Ele retorna um TimeSpan vazio. Um conversor personalizado deve ser usado para desserializar o objeto.

DotNet Info

`.NET Core SDK (refletindo qualquer global.json):
Versão: 3.1.302
Commit: 41faccf259

Ambiente de execução:
Nome do SO: Windows
Versão do sistema operacional: 10.0.20201
Plataforma do sistema operacional: Windows
RID: win10-x64
Caminho de base: C: \ Arquivos de programas \ dotnet \ sdk3.1.302 \

Host (útil para suporte):
Versão: 3.1.6
Commit: 3acd9b0cd1

SDKs do .NET Core instalados:
3.1.302 [C: \ Arquivos de programas \ dotnet \ sdk]

Tempos de execução do .NET Core instalados:
Microsoft.AspNetCore.All 2.1.20 [C: \ Arquivos de programas \ dotnet \ shared \ Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.20 [C: \ Arquivos de programas \ dotnet \ shared \ Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.6 [C: \ Arquivos de programas \ dotnet \ shared \ Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.20 [C: \ Arquivos de programas \ dotnet \ shared \ Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.6 [C: \ Arquivos de programas \ dotnet \ shared \ Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.6 [C: \ Arquivos de programas \ dotnet \ shared \ Microsoft.WindowsDesktop.App] `

Gostaríamos de resolver isso no .NET 6.0 e agradeceríamos uma contribuição da comunidade aqui. A solução nesse meio tempo é usar um conversor personalizado, por exemplo, https://github.com/dotnet/runtime/issues/29932#issuecomment -540200476. A implementação deve ser baseada no formato ISO8601 para que a duração seja consistente com o comportamento de DateTime e DateTimeOffset .

Gostaríamos de resolver isso no .NET 6.0

.NET 5 ainda não foi lançado, eu adoraria se fôssemos curtos na v5 (o semver permite grandes mudanças v3 => v5) em vez de esperar pela v6.

Não foi possível ajustar esse recurso no 5.0. Tivemos que priorizar junto com o resto do trabalho do recurso 5.0 , e este não se encaixou. FWIW isso estava perto da linha de corte, portanto, o movimento tardio.

Apenas para destacar a solução alternativa simples com um conversor personalizado novamente:

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 , posso aguentar por 6.0.

Eu recomendaria usar a cultura invariável ao optar por construir um conversor JSON:

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));
    }
}

Estou trabalhando com asp.net core 3.1 e encontrei esse problema hoje. Estou usando a grade teleriks com signalr e edição em linha em linhas que contêm intervalos de tempo anuláveis ​​e posso ver que a carga útil contém os dados de intervalos de tempo, mas a desserialização falha. Acabei usando a biblioteca Macross.Json.Extensions e retrabalhando parte do meu javascript para fazê-lo funcionar, embora não seja o ideal, espero que consiga uma correção adequada.

E se estiver procurando pelo formato ISO8601, você pode usar este:
`` `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);
    }
}

`` `

Por favor, considere TimeSpan de pelo menos 24 horas .. por exemplo, "24:00:00" deve ser equivalente a new TimeSpan(24, 0, 0)

@gojanpaolo

Considere TimeSpan de pelo menos 24 horas. Por exemplo, "24:00:00" deve ser equivalente ao novo TimeSpan (24, 0, 0)

Isso não é realmente uma coisa JSON, é parte da lógica de análise TimeSpan. Resposta curta: Use "1,00: 00: 00" por 24 horas. Resposta longa: eu fui espelhar o código de tempo de execução um tempo atrás para descobrir por que "24:00:00" não analisa como esperado e escrevi um post no blog sobre isso.

Esta página foi útil?
0 / 5 - 0 avaliações