Runtime: JsonSerializer-Unterstützung für TimeSpan in 3.0?

Erstellt am 18. Juni 2019  ·  36Kommentare  ·  Quelle: dotnet/runtime

_Entschuldigung, falls dies schon woanders beantwortet wurde, aber wenn meine Suchfähigkeiten versagt haben._

Ich teste die Vorschau 6 von 3.0 mit etwas vorhandenem Produktionsanwendungscode, und eines der Probleme, die beim Testen aufgetreten sind, ist, dass einige unserer vorhandenen Objekte, die wir in unseren API-Verträgen verwenden, Eigenschaften verwenden, die TimeSpan Werte, die dann als Strings dargestellt werden.

Ist die Unterstützung für TimeSpan Eigenschaften für 3.0 für die neuen System.Text.Json-APIs geplant?

Wenn dies nicht der Fall sein sollte, müssen wir vor September einige Refactorings durchführen, um sie in Strings umzuwandeln, damit wir den neuen Serializer verwenden können damit das funktioniert.

Unten finden Sie einen minimalen Repro-Komponententest, der den Fehler bei der Behandlung von TimeSpan im Vergleich zu unserem vorhandenen JSON .NET-Code zeigt.

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

Hilfreichster Kommentar

  • Es ist unklar, ob es eine Standardcodierung für Zeitspannen gibt.

Die Standardcodierung für Zeitspannen wäre die "Dauer" von ISO8601 . Dies ist auch das, was XML verwendet, um TimeSpan darzustellen, und ist bereits in XmlConvert und XsdDuration implementiert:

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

Nun sollte TimeSpan selbst das Parsen und Formatieren von ISO8601-Dauerformaten unterstützen. Vielleicht wäre es besser, als ersten Schritt eine neue Standardformatzeichenfolge TimeSpan hinzuzufügen, ähnlich dem DateTime[Offset] ? Danach einen Konverter hinzuzufügen wäre wirklich einfach.

Alle 36 Kommentare

Wir haben derzeit nicht vor, TimeSpan und würden in Zukunft hinzugefügt werden, aber ich kann einige Nachforschungen anstellen, um zu sehen, wie viel Arbeit damit verbunden ist. Alternativ können Sie als Problemumgehung Ihre eigenen JsonConverter erstellen, um TimeSpan zu unterstützen. Ende nächster Woche gebe ich ein Update. Danke.

Wenn wir es hinzufügen würden, würden wir auch TimeSpan APIs zum Reader/Writer/JsonElement hinzufügen, die eine API-Überprüfung durchlaufen müssten.

Danke - ein benutzerdefinierter Konverter wäre auch eine einfache Möglichkeit, damit unsere App für 3.0 funktioniert. Sollen diese Funktionen mit Preview 7 ausgeliefert werden?

Ja, die Unterstützung für benutzerdefinierte Konverter befindet sich jetzt in Master und wird daher in Vorschau 7 verfügbar sein.

Da TimeSpan jedoch ein gängiger BCL-Typ ist, sollten wir dennoch einen Standardkonverter bereitstellen.

Wir haben das heute überprüft:

  • Es gibt ein Argument dafür, da es sich um einen Werttyp handelt. Im Prinzip könnten wir das Parsing so optimieren, dass es frei von Zuweisungen ist (dh das Durchlaufen eines Strings vermeiden).
  • Es ist unklar, ob es eine Standardcodierung für Zeitspannen gibt.
  • Vorerst dicht. Dies kann wieder geöffnet werden, wenn genügend Kundennachweise vorliegen, die dies erforderlich machen.
  • Es ist unklar, ob es eine Standardcodierung für Zeitspannen gibt.

Die Standardcodierung für Zeitspannen wäre die "Dauer" von ISO8601 . Dies ist auch das, was XML verwendet, um TimeSpan darzustellen, und ist bereits in XmlConvert und XsdDuration implementiert:

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

Nun sollte TimeSpan selbst das Parsen und Formatieren von ISO8601-Dauerformaten unterstützen. Vielleicht wäre es besser, als ersten Schritt eine neue Standardformatzeichenfolge TimeSpan hinzuzufügen, ähnlich dem DateTime[Offset] ? Danach einen Konverter hinzuzufügen wäre wirklich einfach.

Dies änderte auch das Standardverhalten der Übergabe von TimeSpan Typen durch AspNetCore in der API, siehe dotnet/AspnetCore#11724. Dies könnte eine bahnbrechende Veränderung sein.

Die einfachste Lösung besteht darin, einen benutzerdefinierten Konverter zu erstellen:

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

Apropos Kundenbeweis: Für unsere Software-Entwicklung ist Support für TimeSpan dringend notwendig.

Ich bin heute darauf gestoßen, eine App auch auf .NET Core 3.0 zu portieren. Bedeutet dies, dass Microsoft keine Pläne hat, nativen Support hinzuzufügen, da dies geschlossen ist? Der Kommentar von

Wiedereröffnung für 5.0 basierend auf zusätzlichen Anfragen. Die ISO8601-Dauer ist wahrscheinlich die beste Darstellung, obwohl auch die Kompatibilität mit Newtonsoft berücksichtigt werden sollte.

Bin gerade heute auf dieses Problem gestoßen. Das Standardverhalten ist schlimmer als das vorherige Verhalten und war völlig unerwartet. Wir sollten es beheben, entweder indem wir ISO8601 verwenden oder mit Newtonsoft kompatibel sind.

Das Standardverhalten ist schlimmer als das vorherige Verhalten und war völlig unerwartet.

@mfeingol Welches Verhalten? Dass es einfach scheitert?

Wir sollten es beheben, entweder indem wir ISO8601 verwenden oder mit Newtonsoft kompatibel sind.

Es ist wirklich einfach, den erwähnten Workaround @rezabayesteh hinzuzufügen.

@khellang : Was ich bei einem relativ Timespan? HasValue Feld als

Es ist wirklich einfach, den Workaround einfach hinzuzufügen

Das habe ich getan, aber das sollte für einen so häufig verwendeten Typ nicht erforderlich sein.

Ich habe dieses Problem heute gefunden (von meinem Kunden gemeldet) und muss alle meine aspnet-Webapi und Apps zurückschalten, um den Newtonsoft.Json-Serializer zu verwenden, anstatt die Konfiguration in Startup.cs zu verwenden:

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

In meinen Fällen verwende ich ein paar Nullable TimeSpan (TimeSpan?) und System.Text.Json hat es serialisiert als:

{
"hasValue": wahr,
"Wert": {
"Häkchen":0,
"Tage": 0,
"Stunden": 0,
"Millisekunden": 0,
"Minuten": 0,
"Sekunden": 0,
"totalDays": 0,
"totalHours": 0,
"totalMillisekunden": 0,
"totalMinutes": 0,
"totalSeconds": 0
}
}

Dies verursacht ein kleines Problem für Javascript-Objekte in Webbrowsern sowie für verschiedene plattformübergreifende (dh verschiedene Programmiersprachen) Deserializer, die meine APIs verbrauchen.

Ich würde das gleiche Serialisierungsergebnis erwarten wie der Newtonsoft.Json-Serializer:

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

Ich habe dieses Problem erst heute gefunden (von meinem Kunden gemeldet) und muss alle meine aspnet-Webapi und -Apps zurückschalten, um stattdessen den Newtonsoft.Json-Serializer zu verwenden

@bashocz Warum https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476 erwähnte Problemumgehung nicht für Sie?

Warum funktioniert die in Problemumgehung bei Ihnen nicht?

@khellang Ich möchte nur hervorheben, dass die TimeSpan-Serialisierung ein Problem für andere Entwickler ist, und ihr Aufmerksamkeit schenken. Ich würde sehr gerne zu System.Text.Json wechseln, es gibt jedoch einige Hindernisse.

Ich hatte vor ein paar Wochen das neue System.Text.Json untersucht und festgestellt, dass es nicht vollständig ist. Ich habe ein Problem mit dotnet/corefx#41747 angesprochen und wurde auf andere dotnet/corefx#39031, dotnet/corefx#41325, dotnet/corefx#38650 hingewiesen. Aus diesem Grund verwenden alle unsere internen Microservices weiterhin Newtonsoft.Json.

Aus unbekannten Gründen habe ich vergessen, Entwickler zu verwalten, um das Problem auch in öffentlichen APIs und Webapps zu beheben.

Übrigens: Ich versuche, Workarounds im Produktionscode so weit wie möglich zu vermeiden. Es ist schwierig, es in Zukunft zu warten und zu entfernen.

@khellang , es ist nicht so, dass eine

@arisewanggithub Controllern stehen globale Einstellungen zur Verfügung. Sie können es über AddJsonOptions() konfigurieren oder eine JsonSerializerOptions Instanz an die Json() Controller-Methode übergeben.

Ist das geschlossen, weil wir einen Workaround haben?!

Ist das geschlossen, weil wir einen Workaround haben?!

Das Problem ist noch offen und wartet auf einen API-/Verhaltensvorschlag, eine Überprüfung und Implementierung. In der Zwischenzeit ist es ziemlich einfach, dies zu umgehen, indem Sie den in https://github.com/dotnet/corefx/issues/38641#issuecomment -540200476 erwähnten Konverter verwenden.

Für alle, die dadurch blockiert werden, hier ist ein NuGet-Paket mit einem Konverter (JsonTimeSpanConverter), den wir vor dem Drop von 5.0 verwenden können: Macross.Json.Extensions

Unterstützt TimeSpan- und Nullable<TimeSpan>-Typen.

Oh, Scheiße, das tut weh!

Für alle, die dadurch blockiert werden, hier ist ein NuGet-Paket mit einem Konverter (JsonTimeSpanConverter), den wir vor dem Drop von 5.0 verwenden können: Macross.Json.Extensions

Unterstützt TimeSpan & NullableTypen.

Laut meinen Tests werden Nullable-Werttypen bereits vom Framework behandelt. Sie brauchen nicht viel mehr als die folgenden:

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: Nach einem genaueren Blick auf die Framework-Quellen stellte sich heraus, dass die Prüfungen auf Null-Token/Null-Werte überflüssig sind. Der obige Code wurde entsprechend aktualisiert.)

Dann konfigurieren Sie JsonSerializerOptions wie folgt:

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

Ich glaube nicht, dass es eine gute Idee ist, eine Abhängigkeit von Drittanbietern hinzuzufügen, wenn Sie mit so wenigen Zeilen zusätzlichen Codes zurechtkommen. Wir sind doch nicht im NPM-Land. ;)

Natürlich wäre es am besten, wenn wir für einen so einfachen Typ überhaupt keinen Workaround bräuchten.

@adams85 FYI Nullable-Werttypen sind fehlerhaft < .NET 5, wenn sie mit JsonConverterAttribute verwendet werden. Siehe #32006. Ich persönlich bevorzuge es, den Attributstil der globalen Konfiguration vorzuziehen, daher die Erweiterungen und die Fehlerbehebung, aber Ihr Konverter ist ausgezeichnet. Macht es Ihnen etwas aus, wenn ich es Macross.Json.Extensions hinzufüge, um anderen zu helfen, die ebenfalls davon profitieren könnten und denen es nichts ausmacht, gelegentlich ins "NPM-Land" zu reisen? :)

@CodeBlanch Vielen Dank, dass Sie auf diese Eigenart mit JsonConverterAttribute hingewiesen haben. Es ist definitiv ein Teil der ganzen Geschichte.

Und natürlich können Sie meinen Konverter zu Ihrer Bibliothek hinzufügen, wenn Sie möchten. :)

Ab .NET 5.0 Preview 5 erhalte ich Cannot skip tokens on partial JSON Fehler beim Roundtrip meiner Entitäten zu JSON mit System.Text.Json. Die anstößige Zeile und das anstößige Zeichen ist der Doppelpunkt in "Ticks":

Wie auch immer, wenn ich den Workaround verwende und TimeSpan auf vernünftige Weise serialisiere, funktioniert es gut.

+1 von mir, weil meine erste Reaktion beim Öffnen der .json Datei, um sie zu untersuchen, nur um die TimeSpan in all ihrer strukturellen Pracht serialisiert zu finden, "sicherlich nicht..." war.

Von @jsedlak in https://github.com/dotnet/runtime/issues/42356 :

Ausgabetitel

Die System.Text.Json.JsonSerializer-Klasse kann eine TimeSpan-Eigenschaft nicht deserialisieren, obwohl sie sie serialisieren kann.

Allgemein

Ein Beispielprojekt, das dieses Problem demonstriert, ist hier verfügbar: https://github.com/jsedlak/TestTimeSpan

Wenn Sie eine TimeSpan-Eigenschaft in einer WebApi zurückgeben, wird sie korrekt in JSON serialisiert:

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

Aber die Standard-Deserializer-Erweiterungen ( HttpClient.GetFromJsonAsync ) können die Eigenschaft nicht verarbeiten. Es gibt eine leere TimeSpan zurück. Zum Deserialisieren des Objekts muss ein benutzerdefinierter Konverter verwendet werden.

DotNet-Info

`.NET Core SDK (entspricht einem global.json):
Version: 3.1.302
Commit: 41faccf259

Laufzeitumgebung:
Betriebssystemname: Windows
Betriebssystemversion: 10.0.20201
Betriebssystemplattform: Windows
RID: win10-x64
Basispfad: C:\Programme\dotnet\sdk3.1.302\

Gastgeber (nützlich für den Support):
Version: 3.1.6
Commit: 3acd9b0cd1

.NET Core SDKs installiert:
3.1.302 [C:\Programme\dotnet\sdk]

.NET Core-Runtimes installiert:
Microsoft.AspNetCore.All 2.1.20 [C:\Programme\dotnet\shared\Microsoft.AspNetCore.All]
Microsoft.AspNetCore.App 2.1.20 [C:\Programme\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.AspNetCore.App 3.1.6 [C:\Programme\dotnet\shared\Microsoft.AspNetCore.App]
Microsoft.NETCore.App 2.1.20 [C:\Programme\dotnet\shared\Microsoft.NETCore.App]
Microsoft.NETCore.App 3.1.6 [C:\Programme\dotnet\shared\Microsoft.NETCore.App]
Microsoft.WindowsDesktop.App 3.1.6 [C:\Programme\dotnet\shared\Microsoft.WindowsDesktop.App]`

Wir möchten dies in .NET 6.0 ansprechen und würden uns über einen Community-Beitrag hier freuen. Die Problemumgehung besteht in der Zwischenzeit darin, einen benutzerdefinierten Konverter zu verwenden, z. B. https://github.com/dotnet/runtime/issues/29932#issuecomment -540200476. Die Implementierung sollte auf dem ISO8601-Format basieren, damit die Dauer dem Verhalten für DateTime und DateTimeOffset .

Wir möchten dies in .NET 6.0 ansprechen

.NET 5 ist noch nicht veröffentlicht, ich würde mich freuen, wenn wir in v5 in den sauren Apfel beißen (semver erlaubt größere Breaking Changes v3 => v5), anstatt auf v6 zu warten.

Wir konnten diese Funktion nicht in 5.0 integrieren. Wir mussten zusammen mit dem Rest der 5.0-Feature-Arbeit Prioritäten setzen, und dieses passte nicht. FWIW war dies nahe der Cut-Line, also der späte Zug.

Um die einfache Problemumgehung mit einem benutzerdefinierten Konverter noch einmal hervorzuheben:

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 , ich kann das für 6.0 nehmen.

Ich würde empfehlen, die invariante Kultur zu verwenden, wenn Sie sich dafür entscheiden, einen Json-Konverter selbst zu erstellen:

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

Ich arbeite mit asp.net Core 3.1 und bin heute auf dieses Problem gestoßen. Ich verwende Teleriks Grid mit Signalr und Inline-Bearbeitung für Zeilen, die nullbare Zeitspannen enthalten und kann sehen, dass die Nutzlast die Zeitspannendaten enthält, aber die Deserialisierung schlägt fehl. Am Ende habe ich die Macross.Json.Extensions-Bibliothek verwendet und einen Teil meines Javascript überarbeitet, damit es funktioniert, aber nicht ideal, ich hoffe, dies wird richtig behoben.

Und wenn Sie nach dem ISO8601-Format suchen, können Sie dies verwenden:
```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);
    }
}

```

Bitte denken Sie an TimeSpan von mindestens 24 Stunden.. zB "24:00:00" sollte new TimeSpan(24, 0, 0)

@gojanpaolo

Bitte beachten Sie eine TimeSpan von mindestens 24 Stunden. zB sollte "24:00:00" der neuen TimeSpan(24, 0, 0) entsprechen.

Das ist nicht wirklich eine JSON-Sache, sondern Teil der TimeSpan-Parsing-Logik. Kurze Antwort: Verwenden Sie "1.00:00:00" für 24 Stunden. Lange Antwort: Ich habe vor einiger Zeit den Laufzeitcode durchforstet, um herauszufinden, warum "24:00:00" nicht wie erwartet geparst wird, und habe einen Blogbeitrag darüber geschrieben.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

omariom picture omariom  ·  3Kommentare

Timovzl picture Timovzl  ·  3Kommentare

noahfalk picture noahfalk  ·  3Kommentare

yahorsi picture yahorsi  ·  3Kommentare

jzabroski picture jzabroski  ·  3Kommentare