Json.NET peut-il sérialiser / désérialiser vers / depuis un flux?

151

J'ai entendu dire que Json.NET est plus rapide que DataContractJsonSerializer, et je voulais l'essayer ...

Mais je n'ai pas trouvé de méthodes sur JsonConvert qui prennent un flux plutôt qu'une chaîne.

Pour désérialiser un fichier contenant JSON sur WinPhone, par exemple, j'utilise le code suivant pour lire le contenu du fichier dans une chaîne, puis désérialiser en JSON. Il semble être environ 4 fois plus lent dans mes tests (très ad hoc) que d'utiliser DataContractJsonSerializer pour désérialiser directement à partir du flux ...

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

Est-ce que je fais mal?

Omri Gazitt
la source

Réponses:

58

MISE À JOUR: Cela ne fonctionne plus dans la version actuelle, voir ci - dessous pour la bonne réponse ( pas besoin de voter contre, c'est correct sur les anciennes versions ).

Utilisez la JsonTextReaderclasse avec a StreamReaderou utilisez la JsonSerializersurcharge qui prend StreamReaderdirectement un :

var serializer = new JsonSerializer();
serializer.Deserialize(streamReader);
Paul Tyng
la source
23
À peu près sûr que cela ne fonctionne plus. Vous devez utiliser un JsonReader ou TextReader
BradLaney
8
Vous souhaiterez peut-être inclure le numéro de version sur lequel il travaille encore pour que les gens sachent quand faire défiler vers le bas.
PoeHaH
@BradLaney yup JsonTextReader (givenStreamReader) est la voie à suivre maintenant
Antoine Meltzheim
Merci d'avoir pris le temps de modifier votre réponse concernant son état de fonctionnement et sa recommandation de réponse
Nick Bull
281

La version actuelle de Json.net ne vous permet pas d'utiliser le code de réponse accepté. Une alternative actuelle est:

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

Documentation: désérialiser JSON à partir d'un flux de fichiers

James Newton-King
la source
4
JsonTextReader fermera son StreamReader par défaut, donc cet exemple pourrait être un peu simplifié en construisant le StreamReader dans l'appel au constructeur JsonTextReader.
Oliver Bock
1
Une idée de la façon dont je peux utiliser un convertisseur personnalisé avec ce code? Ne voyez aucun moyen de spécifier un convertisseur à utiliser par le sérialiseur
alwayslearning
1
En fait, j'ai une exception OutOfMemory et j'utilise déjà ce code, à peu près exactement. Ce qui, je crois, va pour dire que ce n'est pas une garantie - si l'objet désérialisé est suffisamment grand et que vous êtes coincé dans un processus 32 bits, vous pouvez toujours obtenir des erreurs de mémoire avec ce code
PandaWood
1
J'obtiens une erreur "Le type ou le nom d'espace de noms" JsonTextReader "est introuvable" ... des suggestions?
hnvasa
1
J'avais besoin d'ajouter stream.Position = 0;pour désérialiser correctement mon 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
la source
2
Merci! Cela m'a aidé à éviter une OutOfMemoryException que je recevais lorsque je sérialisais une très grande collection d'objets en une chaîne, puis j'écrivais cette chaîne dans mon flux (au lieu de simplement sérialiser directement dans le flux).
Jon Schneider le
2
Pourquoi rincer? L'appel Dispose provoqué par le bloc using ne le fait-il pas déjà?
Şafak Gür
comment l'utiliser ?
Sana
2
Note latérale, car cela pourrait aider les autres: si vous utilisez, JsonSerializer ser = JsonSerializer.Create(settings);vous pouvez définir les paramètres à utiliser lors de la dés / sérialisation.
mike
1
Un problème potentiel avec cette Serializeimplémentation est qu'elle ferme le Streampassé en tant qu'argument, ce qui, selon l'application, peut être un problème. Avec .NET 4.5+, vous pouvez éviter ce problème en utilisant une StreamWritersurcharge de constructeur avec un paramètre leaveOpenqui vous permet de laisser le flux ouvert.
Joe
29

J'ai écrit une classe d'extension pour m'aider à désérialiser des sources JSON (chaîne, flux, fichier).

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

Désérialiser est désormais aussi simple que d'écrire:

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

J'espère que cela aidera quelqu'un d'autre.

Tok '
la source
2
Contre : il polluera toutes les chaînes avec les méthodes d'extension. Solutions de contournement : ne déclarez que Using SomeJsonHelpersNamespacesi nécessaire ou supprimez le thismot - clé et utilisez JsonHelpers.CreateFromJsonString(someJsonString) Pro : c'est tellement plus facile à utiliser :)
Tok
1
Bien que cela puisse être considéré comme «polluant», près de la moitié des extensions de l'objet String peuvent être vues de la même manière. Cela étend un objet d'une manière considérée comme utile à toute personne qui passerait constamment de string (json) à JSON.
vipersassassin
L'utilisation Encoding.Defaultest également mauvaise car elle se comportera différemment sur différentes machines (voir le gros avertissement dans le document Microsoft). On s'attend à ce que JSON soit UTF-8 et c'est ce que JsonSerializer attend. Il devrait en être ainsi Encoding.UTF8. Le code tel quel produira des chaînes déformées ou échouera à désérialiser si des caractères non ASCII sont utilisés.
ckuri
17

Je suis arrivé à cette question à la recherche d'un moyen de diffuser une liste d'objets ouverte sur un System.IO.Streamet de les lire à l'autre extrémité, sans mettre en mémoire tampon la liste entière avant l'envoi. (Plus précisément, je diffuse des objets persistants de MongoDB sur l'API Web.)

@Paul Tyng et @Rivers ont fait un excellent travail en répondant à la question initiale, et j'ai utilisé leurs réponses pour construire une preuve de concept pour mon problème. J'ai décidé de publier mon application de console de test ici au cas où quelqu'un d'autre serait confronté au même problème.

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

Notez que vous pouvez recevoir une exception lorsque le AnonymousPipeServerStreamest supprimé, j'ai ignoré cela car cela n'est pas pertinent pour le problème en question.

Blake Mitchell
la source
1
Je dois modifier cela afin de pouvoir obtenir n'importe quel objet JSON complet. Mon serveur et mon client communiquent en envoyant des extraits de JSON afin que le client puisse envoyer {"sign in":{"username":"nick"}}{"buy item":{"_id":"32321123"}}et il doit voir cela comme deux fragments de JSON signalant un événement à chaque fois qu'il lit un fragment. Dans nodejs, cela peut être fait en 3 lignes de code.
Nick Sotiros