Czy Json.NET może serializować / deserializować do / ze strumienia?

151

Słyszałem, że Json.NET jest szybszy niż DataContractJsonSerializer i chciałem spróbować ...

Ale nie mogłem znaleźć żadnych metod w JsonConvert, które pobierają strumień, a nie ciąg.

Na przykład do deserializacji pliku zawierającego JSON na WinPhone używam następującego kodu, aby wczytać zawartość pliku do ciągu, a następnie deserializować do formatu JSON. Wydaje się, że w moich (bardzo ad-hoc) testach jest około 4 razy wolniejsze niż przy użyciu DataContractJsonSerializer do deserializacji bezpośrednio ze strumienia ...

// DCJS
DataContractJsonSerializer dc = new DataContractJsonSerializer(typeof(Constants));
Constants constants = (Constants)dc.ReadObject(stream);

// JSON.NET
string json = new StreamReader(stream).ReadToEnd();
Constants constants = JsonConvert.DeserializeObject<Constants>(json);

Czy robię to źle?

Omri Gazitt
źródło

Odpowiedzi:

58

AKTUALIZACJA: To już nie działa w aktualnej wersji, patrz poniżej, aby uzyskać poprawną odpowiedź ( nie trzeba głosować, jest to poprawne w starszych wersjach ).

Użyj JsonTextReaderklasy z a StreamReaderlub użyj JsonSerializerprzeciążenia, które pobiera StreamReaderbezpośrednio:

var serializer = new JsonSerializer();
serializer.Deserialize(streamReader);
Paul Tyng
źródło
23
Jestem prawie pewien, że to już nie działa. Musisz użyć JsonReader lub TextReader
BradLaney
8
Możesz dołączyć numer wersji, nad którą nadal pracuje, aby ludzie wiedzieli, kiedy przewinąć w dół.
PoeHaH
@BradLaney yup JsonTextReader (givenStreamReader) to droga do zrobienia teraz
Antoine Meltzheim
Dziękujemy za poświęcenie czasu, aby edytować swoją odpowiedź re to status i rekomendację odpowiedź pracy
Nick Bull
281

Obecna wersja Json.net nie pozwala na użycie zaakceptowanego kodu odpowiedzi. Aktualną alternatywą jest:

public static object DeserializeFromStream(Stream stream)
{
    var serializer = new JsonSerializer();

    using (var sr = new StreamReader(stream))
    using (var jsonTextReader = new JsonTextReader(sr))
    {
        return serializer.Deserialize(jsonTextReader);
    }
}

Dokumentacja: Deserializacja formatu JSON ze strumienia pliku

James Newton-King
źródło
4
JsonTextReader domyślnie zamyka StreamReader, więc ten przykład można nieco uprościć, konstruując StreamReader w wywołaniu konstruktora JsonTextReader.
Oliver Bock
1
Masz jakiś pomysł, jak mogę użyć niestandardowego konwertera wraz z tym kodem? Nie widzę możliwości określenia konwertera, który ma być używany przez serializator
zawsze uczę się
1
Właściwie mam wyjątek OutOfMemory i już używam tego kodu, prawie dokładnie. Co, jak sądzę, mówi, że nie jest to gwarancja - jeśli zdeserializowany obiekt jest wystarczająco duży i utkniesz w 32-bitowym procesie, nadal możesz uzyskać błędy pamięci z tym kodem
PandaWood
1
pojawia się błąd „Nie można znaleźć typu lub nazwy przestrzeni nazw„ JsonTextReader ””… jakieś sugestie?
hnvasa,
1
Musiałem dodać, stream.Position = 0;aby poprawnie zdeserializować mój plik JSON.
hybrid2102,
76
public static void Serialize(object value, Stream s)
{
    using (StreamWriter writer = new StreamWriter(s))
    using (JsonTextWriter jsonWriter = new JsonTextWriter(writer))
    {
        JsonSerializer ser = new JsonSerializer();
        ser.Serialize(jsonWriter, value);
        jsonWriter.Flush();
    }
}

public static T Deserialize<T>(Stream s)
{
    using (StreamReader reader = new StreamReader(s))
    using (JsonTextReader jsonReader = new JsonTextReader(reader))
    {
        JsonSerializer ser = new JsonSerializer();
        return ser.Deserialize<T>(jsonReader);
    }
}
ygaradon
źródło
2
Dzięki! Pomogło mi to uniknąć wyjątku OutOfMemoryException, który otrzymywałem, gdy serializowałem bardzo dużą kolekcję obiektów do ciągu, a następnie zapisywałem ten ciąg w moim strumieniu (zamiast po prostu serializować bezpośrednio do strumienia).
Jon Schneider,
2
Dlaczego spłukiwać? Czy wywołanie Dispose spowodowane przez blok using nie robi już tego?
Şafak Gür
Jak tego użyć ?
Sana
2
Uwaga dodatkowa, ponieważ może to pomóc innym: jeśli używasz JsonSerializer ser = JsonSerializer.Create(settings);, możesz zdefiniować ustawienia, które mają być używane podczas de / serializacji.
Mike
1
Potencjalnym problemem związanym z tą Serializeimplementacją jest to, że zamyka ona Streamprzekazywany jako argument, co w zależności od aplikacji może stanowić problem. Dzięki .NET 4.5+ możesz uniknąć tego problemu, używając StreamWriterprzeciążenia konstruktora z parametrem, leaveOpenktóry pozwala pozostawić otwarty strumień.
Joe
29

Napisałem klasę rozszerzenia, aby pomóc mi w deserializacji ze źródeł JSON (ciąg, strumień, plik).

public static class JsonHelpers
{
    public static T CreateFromJsonStream<T>(this Stream stream)
    {
        JsonSerializer serializer = new JsonSerializer();
        T data;
        using (StreamReader streamReader = new StreamReader(stream))
        {
            data = (T)serializer.Deserialize(streamReader, typeof(T));
        }
        return data;
    }

    public static T CreateFromJsonString<T>(this String json)
    {
        T data;
        using (MemoryStream stream = new MemoryStream(System.Text.Encoding.Default.GetBytes(json)))
        {
            data = CreateFromJsonStream<T>(stream);
        }
        return data;
    }

    public static T CreateFromJsonFile<T>(this String fileName)
    {
        T data;
        using (FileStream fileStream = new FileStream(fileName, FileMode.Open))
        {
            data = CreateFromJsonStream<T>(fileStream);
        }
        return data;
    }
}

Deserializacja jest teraz tak prosta, jak pisanie:

MyType obj1 = aStream.CreateFromJsonStream<MyType>();
MyType obj2 = "{\"key\":\"value\"}".CreateFromJsonString<MyType>();
MyType obj3 = "data.json".CreateFromJsonFile<MyType>();

Mam nadzieję, że pomoże to komuś innemu.

Tok '
źródło
2
Przeciw : zanieczyszcza wszystkie ciągi za pomocą metod rozszerzających. Obejście : zadeklaruj tylko Using SomeJsonHelpersNamespacetam, gdzie to konieczne, lub usuń thissłowo kluczowe i użyj JsonHelpers.CreateFromJsonString(someJsonString) Pro : jest tak łatwiejszy w użyciu :)
Tok '
1
Chociaż mogłoby to być postrzegane jako "zanieczyszczające", prawie połowa rozszerzeń w obiekcie String mogła być postrzegana w ten sam sposób. To rozszerza obiekt w sposób postrzegany jako przydatny dla każdego, kto konsekwentnie zmieniałby się z string (json) na JSON.
vipersassassin
Również używanie Encoding.Defaultjest złe, ponieważ będzie zachowywać się inaczej na różnych komputerach (zobacz duże ostrzeżenie w dokumentacji Microsoft). JSON ma być UTF-8 i tego oczekuje JsonSerializer. Tak powinno być Encoding.UTF8. Kod taki, jaki jest, będzie generował zniekształcone ciągi lub zakończy się niepowodzeniem w deserializacji, jeśli zostaną użyte znaki inne niż ASCII.
ckuri
17

Doszedłem do tego pytania, szukając sposobu na przesłanie otwartej listy obiektów na a System.IO.Streami odczytanie ich z drugiego końca, bez buforowania całej listy przed wysłaniem. (W szczególności przesyłam strumieniowo utrwalone obiekty z MongoDB przez interfejs API sieci Web).

@Paul Tyng i @Rivers wykonali świetną robotę, odpowiadając na pierwotne pytanie, a ja wykorzystałem ich odpowiedzi, aby stworzyć dowód słuszności mojego problemu. Postanowiłem opublikować tutaj moją aplikację na konsolę testową na wypadek, gdyby ktoś inny miał ten sam problem.

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace TestJsonStream {
    class Program {
        static void Main(string[] args) {
            using(var writeStream = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.None)) {
                string pipeHandle = writeStream.GetClientHandleAsString();
                var writeTask = Task.Run(() => {
                    using(var sw = new StreamWriter(writeStream))
                    using(var writer = new JsonTextWriter(sw)) {
                        var ser = new JsonSerializer();
                        writer.WriteStartArray();
                        for(int i = 0; i < 25; i++) {
                            ser.Serialize(writer, new DataItem { Item = i });
                            writer.Flush();
                            Thread.Sleep(500);
                        }
                        writer.WriteEnd();
                        writer.Flush();
                    }
                });
                var readTask = Task.Run(() => {
                    var sw = new Stopwatch();
                    sw.Start();
                    using(var readStream = new AnonymousPipeClientStream(pipeHandle))
                    using(var sr = new StreamReader(readStream))
                    using(var reader = new JsonTextReader(sr)) {
                        var ser = new JsonSerializer();
                        if(!reader.Read() || reader.TokenType != JsonToken.StartArray) {
                            throw new Exception("Expected start of array");
                        }
                        while(reader.Read()) {
                            if(reader.TokenType == JsonToken.EndArray) break;
                            var item = ser.Deserialize<DataItem>(reader);
                            Console.WriteLine("[{0}] Received item: {1}", sw.Elapsed, item);
                        }
                    }
                });
                Task.WaitAll(writeTask, readTask);
                writeStream.DisposeLocalCopyOfClientHandle();
            }
        }

        class DataItem {
            public int Item { get; set; }
            public override string ToString() {
                return string.Format("{{ Item = {0} }}", Item);
            }
        }
    }
}

Zauważ, że możesz otrzymać wyjątek, gdy AnonymousPipeServerStreamzostanie usunięty, zignorowałem to, ponieważ nie ma to związku z problemem.

Blake Mitchell
źródło
1
Muszę to zmodyfikować, aby uzyskać dowolny kompletny obiekt JSON. Mój serwer i klient komunikują się, wysyłając fragmenty kodu JSON, aby klient mógł wysłać, {"sign in":{"username":"nick"}}{"buy item":{"_id":"32321123"}}i musi to zobaczyć jako dwa fragmenty JSON sygnalizujące zdarzenie za każdym razem, gdy odczytuje fragment. W nodejs można to zrobić w 3 wierszach kodu.
Nick Sotiros