Runtime: Поддержка JsonSerializer для TimeSpan в 3.0?

Созданный на 18 июн. 2019  ·  36Комментарии  ·  Источник: dotnet/runtime

Приношу свои извинения, если на этот вопрос уже ответили где-то еще, но если это были мои поисковые способности, я потерпел неудачу.

Я пробую превью 6 версии 3.0 с некоторым существующим кодом производственного приложения, и одна из проблем, возникающих в результате тестирования, заключается в том, что некоторые из наших существующих объектов, которые мы используем в наших контрактах API, используют свойства, которые являются TimeSpan значения, которые затем представлены в виде строк.

Планируется ли поддержка свойств TimeSpan в версии 3.0 для новых API-интерфейсов System.Text.Json?

Если этого не произойдет, это дает нам уведомление о необходимости провести некоторый рефакторинг до сентября, чтобы преобразовать их в строки, чтобы мы могли использовать новый сериализатор, где, как если бы он планировался, но просто еще не реализован, нам просто нужно дождаться более позднего предварительного просмотра чтобы это работало.

Ниже приведен минимальный модульный тест воспроизведения, демонстрирующий сбой обработки TimeSpan по сравнению с нашим существующим кодом JSON .NET.

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

Самый полезный комментарий

  • Неясно, существует ли стандартная кодировка для временных интервалов.

Стандартной кодировкой временных интервалов будет «продолжительность» ISO8601 . Это также то, что XML использует для представления TimeSpan и уже реализовано в XmlConvert и 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

Теперь, возможно, сам TimeSpan должен поддерживать синтаксический анализ и форматирование форматов продолжительности ISO8601. Может быть, было бы лучше в качестве первого шага добавить новую стандартную строку формата TimeSpan , аналогичную DateTime[Offset] ("O", "o")? Добавить конвертер после этого было бы очень просто.

Все 36 Комментарий

В настоящее время у нас нет планов по поддержке TimeSpan и мы будем добавлены в будущем, но я могу провести небольшое расследование, чтобы увидеть, сколько работы потребуется. В качестве альтернативы вы можете создать свой собственный JsonConverter для поддержки TimeSpan в качестве обходного пути. Я дам обновленную информацию к концу следующей недели. Спасибо.

Если бы мы добавили его, мы бы также захотели добавить TimeSpan API в reader / writer / JsonElement, который должен был бы пройти проверку api.

Спасибо - настраиваемый конвертер также был бы достаточно простым способом заставить его работать в нашем приложении для версии 3.0. Планируется ли появление этих возможностей в предварительной версии 7?

Да, поддержка настраиваемого конвертера теперь находится в главном и, следовательно, будет в предварительной версии 7.

Однако, поскольку TimeSpan является распространенным типом BCL, мы все же должны предоставить преобразователь по умолчанию.

Мы рассмотрели это сегодня:

  • Есть аргумент в пользу этого, потому что это тип значения. В принципе, мы могли бы оптимизировать синтаксический анализ так, чтобы он не занимал места (т.е. избегал прохождения строки).
  • Неясно, существует ли стандартная кодировка для временных интервалов.
  • А пока закройтесь. Его можно будет открыть повторно, если будет достаточно доказательств того, что это необходимо.
  • Неясно, существует ли стандартная кодировка для временных интервалов.

Стандартной кодировкой временных интервалов будет «продолжительность» ISO8601 . Это также то, что XML использует для представления TimeSpan и уже реализовано в XmlConvert и 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

Теперь, возможно, сам TimeSpan должен поддерживать синтаксический анализ и форматирование форматов продолжительности ISO8601. Может быть, было бы лучше в качестве первого шага добавить новую стандартную строку формата TimeSpan , аналогичную DateTime[Offset] ("O", "o")? Добавить конвертер после этого было бы очень просто.

Это также изменило поведение по умолчанию того, как AspNetCore возвращает тип TimeSpan в API, см. Dotnet / AspnetCore # 11724. Это может быть критическое изменение.

Самое простое решение - создать собственный конвертер:

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

Говоря о свидетельствах клиентов: для нашей разработки программного обеспечения поддержка TimeSpan крайне необходима.

Попробуй сегодня портировать приложение на .NET Core 3.0. Поскольку это закрыто, означает ли это, что Microsoft не планирует добавлять встроенную поддержку? Комментарий @khellang кажется мне довольно убедительным аргументом в пользу того, что он должен быть где-то в дорожной карте ...

Повторное открытие для 5.0 на основании дополнительных запросов. Продолжительность ISO8601, вероятно, является лучшим представлением, хотя также следует учитывать совместимость с Newtonsoft.

Просто столкнулся с этой проблемой сегодня. Поведение по умолчанию хуже, чем предыдущее, и было совершенно неожиданным. Мы должны исправить это, используя ISO8601 или совместимость с Newtonsoft.

Поведение по умолчанию хуже, чем предыдущее, и было совершенно неожиданным.

@mfeingol Какое поведение? Что это просто не удается?

Мы должны исправить это, используя ISO8601 или совместимость с Newtonsoft.

Очень легко просто добавить обходной путь, упомянутый

@khellang : что я наблюдал с относительно ванильным проектом ASP.NET Core, так это то, что он сериализует Timespan? как поле HasValue , а затем каждое из свойств структуры TimeSpan.

Очень легко просто добавить обходной путь

Я сделал это, но в этом не должно быть необходимости для такого широко используемого типа.

Я только что обнаружил эту проблему сегодня (о которой сообщил мой клиент), и мне нужно переключить все мои aspnet webapi и приложения, чтобы использовать сериализатор Newtonsoft.Json вместо конфигурации в Startup.cs:

services.AddControllers ()
.AddNewtonsoftJson (параметры =>
{
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
});

В моих случаях я использую несколько обнуляемых TimeSpan (TimeSpan?), А System.Text.Json сериализовал его как:

{
"hasValue": правда,
"ценность": {
«галочка»: 0,
«дней»: 0,
«часы»: 0,
«миллисекунды»: 0,
«минут»: 0,
«секунд»: 0,
"totalDays": 0,
«totalHours»: 0,
"totalMilliseconds": 0,
"totalMinutes": 0,
«totalSeconds»: 0
}
}

Это вызывает небольшую проблему для объектов javascript в веб-браузерах, а также для различных кроссплатформенных (т.е. различных языков программирования) десериализаторов, использующих мой API.

Я ожидал бы того же результата сериализации, что и сериализатор Newtonsoft.Json:

{
"timeSpan": "00: 00: 00.0000000",
"nullTimeSpan": нуль
}

Я только что обнаружил эту проблему сегодня (о которой сообщил мой клиент), и мне нужно переключить все мои aspnet webapi и приложения, чтобы вместо этого использовать сериализатор Newtonsoft.Json

@bashocz Почему обходной путь, упомянутый в https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476, вам не подходит?

Почему обходной путь, упомянутый в # 38641 (комментарий), вам не

@khellang Я просто хочу выделить сериализацию TimeSpan - это проблема для других разработчиков и уделить ей внимание. Очень хотелось бы перейти на System.Text.Json, но есть некоторые препятствия.

Несколько недель назад я исследовал новый System.Text.Json и обнаружил, что он неполный. Я поднял проблему dotnet / corefx # 41747 и указал на другие связанные с dotnet / corefx # 39031, dotnet / corefx # 41325, dotnet / corefx # 38650. Из-за этого все наши внутренние микросервисы по-прежнему используют Newtonsoft.Json.

По неизвестной причине я забыл поручить разработчикам исправить это и в общедоступных API, и в веб-приложениях.

Кстати: я стараюсь по возможности избегать обходных путей в производственном коде ... его трудно поддерживать и удалять в будущем.

@khellang , дело не в том, что обходной путь не сработает. Это такая простая вещь, которая не требует от разработчиков добавления обходного пути. Поскольку .NET Core 3 представляет собой большую функцию, в ней не должно быть недостатка в таких базовых реализациях.

@arisewanggithub Для контроллеров доступны глобальные настройки. Вы можете настроить его с помощью AddJsonOptions() , или вы можете передать JsonSerializerOptions экземпляра в Json() контроллера метода.

Это закрытая причина, по которой у нас есть обходной путь ?!

Это закрытая причина, по которой у нас есть обходной путь ?!

Проблема все еще открыта, ожидается предложение, рассмотрение и реализация API / поведения. А пока довольно легко обойтись, используя конвертер, упомянутый в https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476.

Для всех, кто заблокирован этим, вот пакет NuGet с конвертером (JsonTimeSpanConverter), который мы можем использовать перед выпуском 5.0:

Поддерживает типы TimeSpan и Nullable <TimeSpan>.

Вот дерьмо, больно!

Для всех, кто заблокирован этим, вот пакет NuGet с конвертером (JsonTimeSpanConverter), который мы можем использовать перед выпуском 5.0:

Поддерживает TimeSpan и Nullableтипы.

Согласно моим тестам, фреймворк уже обрабатывает типы значений, допускающие значение NULL. Вам не потребуется ничего, кроме следующего:

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

(РЕДАКТИРОВАТЬ: после более внимательного изучения источников фреймворка выяснилось, что проверки нулевых токенов / нулевых значений избыточны. Приведенный выше код был обновлен соответствующим образом.)

Затем настройте JsonSerializerOptions следующим образом:

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

Я не думаю, что добавление сторонней зависимости - хорошая идея, если вы можете справиться с таким несколькими строками дополнительного кода. В конце концов, мы не на земле НПМ. ;)

Конечно, было бы лучше, если бы нам вообще не требовалось обходное решение для такого базового типа.

@ adams85 FYI Типы значений, NULL, ошибаются <.NET 5 при использовании с JsonConverterAttribute. См. № 32006. Я лично предпочитаю использовать стиль атрибута, а не глобальную конфигурацию, поэтому исправлены расширения и ошибки, но ваш конвертер отличный. Вы не возражаете, если я добавлю его в Macross.Json.Extensions, чтобы помочь кому-либо еще, кто также может извлечь из этого выгоду, и не возражает против того, чтобы при случае поехать в «страну НПМ»? :)

@CodeBlanch Спасибо, что указали на эту причуду с помощью JsonConverterAttribute out. Это определенно часть всей истории.

И, конечно же, вы можете добавить мой конвертер в вашу библиотеку, если вам это нравится. :)

Начиная с .NET 5.0 Preview 5, я получал ошибки Cannot skip tokens on partial JSON переводящие мои объекты в JSON с помощью System.Text.Json. Оскорбительная строка и символ - это двоеточие в "Ticks":

В любом случае, если я использую обходной путь и разумно сериализую TimeSpan он работает нормально.

+1 от меня, потому что моя первоначальная реакция при открытии файла .json для его проверки только для того, чтобы найти сериализованный TimeSpan во всей его структурной красоте, была "конечно же не ..."

Из @jsedlak в https://github.com/dotnet/runtime/issues/42356 :

Заголовок выпуска

Класс System.Text.Json.JsonSerializer не может десериализовать свойство TimeSpan, даже если он может сериализовать его.

Общий

Пример проекта, демонстрирующего эту проблему, доступен здесь: https://github.com/jsedlak/TestTimeSpan

Если вы возвращаете свойство TimeSpan в WebApi, оно правильно сериализуется в 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 } } ]

Но расширения десериализатора по умолчанию ( HttpClient.GetFromJsonAsync ) не могут обрабатывать свойство. Он возвращает пустой TimeSpan. Для десериализации объекта необходимо использовать специальный преобразователь.

Информация о DotNet

`.NET Core SDK (отражающий любой global.json):
Версия: 3.1.302
Фиксация: 41faccf259

Среда выполнения:
Имя ОС: Windows
Версия ОС: 10.0.20201
Платформа ОС: Windows
RID: win10-x64
Базовый путь: C: \ Program Files \ dotnet \ sdk3.1.302 \

Хост (полезно для поддержки):
Версия: 3.1.6
Фиксация: 3acd9b0cd1

Установленные SDK .NET Core:
3.1.302 [C: \ Program Files \ dotnet \ sdk]

Установленные среды выполнения .NET Core:
Microsoft.AspNetCore.All 2.1.20 [C: \ Program Files \ dotnet \ shared \ Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.20 [C: \ Program Files \ dotnet \ shared \ Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.6 [C: \ Program Files \ dotnet \ shared \ Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.20 [C: \ Program Files \ dotnet \ shared \ Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.6 [C: \ Program Files \ dotnet \ shared \ Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.6 [C: \ Program Files \ dotnet \ shared \ Microsoft.WindowsDesktop.App] `

Мы хотели бы решить эту проблему в .NET 6.0 и были бы признательны за вклад сообщества. А пока можно использовать специальный конвертер, например https://github.com/dotnet/runtime/issues/29932#issuecomment -540200476. Реализация должна быть основана на формате ISO8601, чтобы продолжительность согласовывалась с поведением для DateTime и DateTimeOffset .

Мы хотели бы решить эту проблему в .NET 6.0.

.NET 5 еще не выпущен, мне бы хотелось, чтобы мы укусили пулю в v5 (semver допускает серьезные критические изменения v3 => v5) вместо того, чтобы ждать v6.

Мы не смогли уместить эту функцию в 5.0. Нам пришлось расставить приоритеты вместе с остальной работой над

Просто чтобы еще раз выделить простой обходной путь с помощью настраиваемого конвертера :

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 , могу на 6.0 взять.

Я бы рекомендовал использовать инвариантную культуру при самостоятельном создании 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));
    }
}

Я работаю с asp.net core 3.1 и сегодня столкнулся с этой проблемой. Я использую сетку teleriks с signalr и встроенным редактированием строк, которые содержат временные интервалы, допускающие значение NULL, и вижу, что полезная нагрузка содержит данные временных интервалов, но десериализация не выполняется. В итоге я использовал библиотеку Macross.Json.Extensions и переделал некоторые из моих javascript, чтобы заставить его работать, хотя и не идеально, надеюсь, это будет исправлено.

И если вы ищете формат ISO8601, вы можете использовать это:
`` С #

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

`` ''

Пожалуйста, примите во внимание TimeSpan не менее 24 часов .. например, "24:00:00" должно быть эквивалентно new TimeSpan(24, 0, 0)

@gojanpaolo

Пожалуйста, примите во внимание, что TimeSpan составляет не менее 24 часов .. например, "24:00:00" должно быть эквивалентно новому TimeSpan (24, 0, 0)

На самом деле это не JSON, это часть логики синтаксического анализа TimeSpan. Краткий ответ: используйте «1,00: 00: 00» в течение 24 часов. Длинный ответ: я некоторое время назад изучал код времени выполнения, чтобы выяснить, почему «24:00:00» не разбирается так, как ожидалось, и написал об этом сообщение в блоге.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги