Runtime: Prise en charge de JsonSerializer pour TimeSpan en 3.0 ?

Créé le 18 juin 2019  ·  36Commentaires  ·  Source: dotnet/runtime

_Mes excuses si cela a déjà été répondu ailleurs, mais si c'était le cas, mes capacités de recherche m'ont échoué._

J'essaie l'aperçu 6 de 3.0 avec du code d'application de production existant, et l'un des problèmes qui ressort des tests est que certains de nos objets existants que nous utilisons dans nos contrats d'API utilisent des propriétés qui sont TimeSpan valeurs, qui sont ensuite représentées sous forme de chaînes.

La prise en charge des propriétés TimeSpan -elle prévue pour la version 3.0 pour les nouvelles API System.Text.Json ?

Si ce n'est pas le cas, cela nous avertit de procéder à une refactorisation avant septembre pour les changer en chaînes afin que nous puissions utiliser le nouveau sérialiseur, où, comme si c'était prévu mais pas encore implémenté, il nous suffit d'attendre un aperçu ultérieur pour que cela fonctionne.

Vous trouverez ci-dessous un test unitaire de reproduction minimal qui démontre l'échec de la gestion de TimeSpan par rapport à notre code JSON .NET existant.

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

Commentaire le plus utile

  • Il n'est pas clair s'il existe un codage standard pour les intervalles de temps.

L'encodage standard pour les intervalles de temps serait la "durée" d'ISO8601 . C'est aussi ce que XML utilise pour représenter TimeSpan et est déjà implémenté dans XmlConvert et 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

Maintenant, sans doute, TimeSpan lui-même devrait prendre en charge l'analyse et le formatage des formats de durée ISO8601. Peut-être serait-il préférable, dans un premier temps, d'ajouter une nouvelle chaîne de format standard TimeSpan , similaire au spécificateur de format aller-retour ("O", "o") de DateTime[Offset] ? Ajouter un convertisseur après cela serait vraiment simple.

Tous les 36 commentaires

Nous n'avons actuellement pas l'intention de prendre en charge TimeSpan et serions ajoutés à l'avenir, mais je peux faire quelques recherches pour voir combien de travail est impliqué. Alternativement, vous pouvez créer votre propre JsonConverter pour prendre en charge TimeSpan comme solution de contournement. Je donnerai une mise à jour d'ici la fin de la semaine prochaine. Merci.

Si nous devions l'ajouter, nous voudrions également ajouter des API TimeSpan au lecteur/écrivain/JsonElement, qui devraient passer par l'examen de l'API.

Merci - un convertisseur personnalisé serait également un moyen assez simple de le faire fonctionner pour notre application pour 3.0. Ces capacités sont-elles prévues pour être livrées avec l'aperçu 7 ?

Oui, le support du convertisseur personnalisé est maintenant dans le maître et sera donc dans l'aperçu 7.

Cependant, étant donné que TimeSpan est un type BCL courant, nous devons toujours fournir un convertisseur par défaut.

Nous avons examiné cela aujourd'hui:

  • Il y a un argument en sa faveur, car c'est un type valeur. En principe, nous pourrions optimiser l'analyse pour qu'elle soit sans allocation (c'est-à-dire éviter de passer par une chaîne).
  • Il n'est pas clair s'il existe un codage standard pour les intervalles de temps.
  • Pour l'instant, fermez. Cela peut être rouvert s'il y a suffisamment de preuves client que cela est nécessaire.
  • Il n'est pas clair s'il existe un codage standard pour les intervalles de temps.

L'encodage standard pour les intervalles de temps serait la "durée" d'ISO8601 . C'est aussi ce que XML utilise pour représenter TimeSpan et est déjà implémenté dans XmlConvert et 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

Maintenant, sans doute, TimeSpan lui-même devrait prendre en charge l'analyse et le formatage des formats de durée ISO8601. Peut-être serait-il préférable, dans un premier temps, d'ajouter une nouvelle chaîne de format standard TimeSpan , similaire au spécificateur de format aller-retour ("O", "o") de DateTime[Offset] ? Ajouter un convertisseur après cela serait vraiment simple.

Cela a également modifié le comportement par défaut de la façon dont AspNetCore traite le retour du type TimeSpan dans l'API, voir dotnet/AspnetCore#11724. Cela pourrait être un changement décisif.

La solution la plus simple consiste à créer un convertisseur personnalisé :

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

En parlant de témoignage client : pour notre support de développement logiciel pour TimeSpan est hautement nécessaire.

J'ai également rencontré cela aujourd'hui en portant une application sur .NET Core 3.0. Parce que cela est fermé, cela signifie-t-il que Microsoft n'a pas l'intention d'ajouter un support natif ? Le commentaire de @khellang me semble un argument assez convaincant selon lequel il devrait figurer quelque part sur une feuille de route...

Réouverture pour 5.0 sur la base de demandes supplémentaires. La durée ISO8601 est probablement la meilleure représentation, bien que la compatibilité avec Newtonsoft doive également être prise en compte.

Je viens de rencontrer ce problème aujourd'hui. Le comportement par défaut est pire que le comportement précédent et était tout à fait inattendu. Nous devrions le corriger, soit en utilisant ISO8601, soit en étant compatible avec Newtonsoft.

Le comportement par défaut est pire que le comportement précédent et était tout à fait inattendu.

@mfeingol Quel comportement ? Qu'il échoue tout simplement ?

Nous devrions le corriger, soit en utilisant ISO8601, soit en étant compatible avec Newtonsoft.

Il est vraiment facile d'ajouter simplement la solution de contournement mentionnée par @rezabayesteh .

@khellang : ce que j'ai observé avec un projet ASP.NET Core relativement Timespan? tant que champ HasValue , puis chacune des propriétés de la structure TimeSpan.

Il est vraiment facile d'ajouter simplement la solution de contournement

Je l'ai fait, mais cela ne devrait pas être nécessaire pour un type aussi couramment utilisé.

Je viens de trouver ce problème aujourd'hui (signalé par mon client) et je dois rétablir l'ensemble de mes webapi et applications aspnet pour utiliser le sérialiseur Newtonsoft.Json au lieu d'utiliser la configuration dans Startup.cs :

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

Dans mes cas, j'utilise quelques TimeSpan nullable (TimeSpan?) Et System.Text.Json l'a sérialisé comme:

{
"hasValue": vrai,
"valeur": {
"tick":0,
"jours": 0,
"heures": 0,
"millisecondes": 0,
"minutes": 0,
"secondes": 0,
"totalJours": 0,
"totalHeures": 0,
"totalMillisecondes": 0,
"totalMinutes": 0,
"totalSecondes": 0
}
}

Cela pose un petit problème pour les objets javascript dans les navigateurs Web, ainsi que pour divers désérialiseurs multiplateformes (c'est-à-dire divers langages de programmation) consommant mes API.

Je m'attendrais au même résultat de sérialisation que le sérialiseur Newtonsoft.Json :

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

Je viens de trouver ce problème aujourd'hui (rapporté par mon client) et je dois revenir à toutes mes applications Webapi et applications aspnet pour utiliser le sérialiseur Newtonsoft.Json à la place

@bashocz Pourquoi la solution de contournement mentionnée dans https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476 ne fonctionnera-t-elle pas pour vous ?

Pourquoi la solution de contournement mentionnée dans #38641 (commentaire) ne fonctionnera-t-elle pas pour vous ?

@khellang Je veux juste souligner que la sérialisation TimeSpan est un problème pour d'autres développeurs, et y prêter attention. J'aimerais beaucoup passer à System.Text.Json, mais il y a quelques obstacles.

J'avais enquêté sur le nouveau System.Text.Json il y a quelques semaines et découvert que sa fonctionnalité n'était pas complète. J'ai soulevé un problème dotnet/corefx#41747 et j'ai été dirigé vers d'autres dotnet/corefx#39031, dotnet/corefx#41325, dotnet/corefx#38650 liés. Pour cette raison, tous nos microservices internes utilisent toujours Newtonsoft.Json.

Pour une raison inconnue, j'ai oublié de gérer les développeurs pour le corriger également sur les API publiques et les applications Web.

BTW : j'essaie d'éviter autant que possible les solutions de contournement dans le code de production. Il est difficile de le maintenir et de le supprimer à l'avenir.

@khellang , ce n'est pas qu'une solution de contournement ne fonctionnera pas. C'est juste qu'une chose si basique ne devrait pas avoir besoin que les développeurs ajoutent une solution de contournement. En tant que grande fonctionnalité introduite pour .NET core 3, il ne devrait pas manquer de telles implémentations de base.

@arisewanggithub Des paramètres globaux sont disponibles pour les contrôleurs. Vous pouvez le configurer via AddJsonOptions() , ou vous pouvez passer un JsonSerializerOptions par exemple à Json() méthode contrôleur.

Est-ce fermé parce que nous avons une solution de contournement ?!

Est-ce fermé parce que nous avons une solution de contournement ?!

Le problème est toujours ouvert, dans l'attente d'une proposition, d'un examen et d'une mise en œuvre d'API/de comportement. En attendant, il est assez facile de contourner ce problème en utilisant le convertisseur mentionné dans https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476.

Pour toute personne bloquée par cela, voici un package NuGet avec un convertisseur (JsonTimeSpanConverter) que nous pouvons utiliser avant la version 5.0 : Macross.Json.Extensions

Prend en charge les types TimeSpan et Nullable<TimeSpan>.

Oh merde, ça fait mal !

Pour toute personne bloquée par cela, voici un package NuGet avec un convertisseur (JsonTimeSpanConverter) que nous pouvons utiliser avant la version 5.0 : Macross.Json.Extensions

Prend en charge TimeSpan et Nullableles types.

D'après mes tests, les types valeur nullable sont déjà gérés par le framework. Vous n'avez pas besoin de beaucoup plus que ce qui suit :

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

(EDIT : après avoir examiné de plus près les sources du framework, il s'est avéré que les vérifications des jetons nuls/valeurs nulles sont redondantes. Le code ci-dessus a été mis à jour en conséquence.)

Configurez ensuite JsonSerializerOptions comme ceci :

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

Je ne pense pas que ce soit une bonne idée d'ajouter une dépendance tierce si vous pouvez gérer si peu de lignes de code supplémentaires. Nous ne sommes pas au pays du NMP après tout. ;)

Bien sûr, ce serait mieux si nous n'avions pas du tout besoin d'une solution de contournement pour un type aussi basique.

@adams85 FYI Les types de valeur Nullable sont bogués < .NET 5 lorsqu'ils sont utilisés avec JsonConverterAttribute. Voir #32006. Personnellement, je préfère utiliser le style d'attribut plutôt que la configuration globale, d'où les extensions et la correction de bogues, mais votre convertisseur est excellent. Cela vous dérange-t-il si je l'ajoute à Macross.Json.Extensions pour aider quelqu'un d'autre qui pourrait également en bénéficier et que cela ne dérange pas de voyager à l'occasion sur "NPM land" ? :)

@CodeBlanch Merci d'avoir signalé cette bizarrerie avec JsonConverterAttribute . Cela fait certainement partie de l'histoire complète.

Et, bien sûr, vous êtes autorisé à ajouter mon convertisseur à votre bibliothèque si vous l'aimez. :)

À partir de .NET 5.0 Preview 5, j'ai reçu des erreurs Cannot skip tokens on partial JSON aller-retour de mes entités vers JSON avec System.Text.Json. La ligne et le caractère incriminés sont les deux points dans "Ticks":

Quoi qu'il en soit, si j'utilise la solution de contournement et sérialise TimeSpan de manière raisonnable, cela fonctionne bien.

+1 de ma part parce que ma réaction initiale lors de l'ouverture du fichier .json pour l'inspecter uniquement pour trouver le TimeSpan sérialisé dans toute sa splendeur structurelle était "certainement pas..."

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

Titre du problème

La classe System.Text.Json.JsonSerializer ne peut pas désérialiser une propriété TimeSpan, même si elle peut la sérialiser.

Général

Un exemple de projet démontrant ce problème est disponible ici : https://github.com/jsedlak/TestTimeSpan

Si vous retournez une propriété TimeSpan dans une WebApi, elle est correctement sérialisée 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 } } ]

Mais les extensions de désérialisation par défaut ( HttpClient.GetFromJsonAsync ) ne peuvent pas gérer la propriété. Il renvoie un TimeSpan vide. Un convertisseur personnalisé doit être utilisé pour désérialiser l'objet.

Informations DotNet

`.NET Core SDK (reflétant tout global.json) :
Version : 3.1.302
Engagement : 41faccf259

Environnement d'exécution:
Nom du système d'exploitation : Windows
Version du système d'exploitation : 10.0.20201
Plate-forme du système d'exploitation : Windows
RID : win10-x64
Chemin de base : C:\Program Files\dotnet\sdk3.1.302\

Hébergeur (utile pour le support) :
Version : 3.1.6
Engagement : 3acd9b0cd1

SDK .NET Core installés :
3.1.302 [C:\Program Files\dotnet\sdk]

Runtimes .NET Core installés :
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]`

Nous aimerions résoudre ce problème dans .NET 6.0 et apprécierions une contribution de la communauté ici. En attendant, la solution de contournement consiste à utiliser un convertisseur personnalisé, par exemple https://github.com/dotnet/runtime/issues/29932#issuecomment -540200476. L'implémentation doit être basée sur le format ISO8601 pour que la durée soit cohérente avec le comportement de DateTime et DateTimeOffset .

Nous aimerions résoudre ce problème dans .NET 6.0

.NET 5 n'est pas encore sorti, j'aimerais bien que nous mordions dans la v5 (semver permet des changements majeurs de rupture v3 => v5) au lieu d'attendre la v6.

Nous ne pouvions pas intégrer cette fonctionnalité dans 5.0. Nous devions établir des priorités avec le reste du travail sur les fonctionnalités 5.0 , et celui-ci ne correspondait pas. FWIW, c'était proche de la ligne de coupe, d'où le mouvement tardif.

Juste pour souligner à nouveau la solution de contournement simple avec un convertisseur personnalisé :

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 , je peux prendre ça pour 6.0.

Je recommanderais d'utiliser la culture invariante lorsque vous choisissez de créer vous-même un convertisseur 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));
    }
}

Je travaille avec asp.net core 3.1 et j'ai rencontré ce problème aujourd'hui. J'utilise la grille teleriks avec signaleur et l'édition en ligne sur les lignes qui contiennent des intervalles de temps nuls et je peux voir que la charge utile contient les données d'intervalles de temps mais que la désérialisation échoue. J'ai fini par utiliser la bibliothèque Macross.Json.Extensions et retravailler une partie de mon javascript pour le faire fonctionner, ce qui n'est pas idéal cependant, j'espère que cela sera correctement corrigé.

Et si vous recherchez le format ISO8601, vous pouvez utiliser ceci :
```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);
    }
}

```

Veuillez considérer TimeSpan d'au moins 24 heures. Par exemple, "24:00:00" devrait être équivalent à new TimeSpan(24, 0, 0)

@gojanpaolo

Veuillez considérer TimeSpan d'au moins 24 heures. Par exemple, "24:00:00" devrait être équivalent à un nouveau TimeSpan(24, 0, 0)

Ce n'est pas vraiment une chose JSON, cela fait partie de la logique d'analyse TimeSpan. Réponse courte : utilisez "1.00:00:00" pendant 24 heures. Réponse longue : j'ai parcouru le code d'exécution il y a quelque temps pour comprendre pourquoi "24:00:00" n'analyse pas comme prévu et j'ai écrit un article de blog à ce sujet.

Cette page vous a été utile?
0 / 5 - 0 notes