使用System.Text.JSON动态格式化JSON流



我有一个Json字符串,它不是独立的,例如:

{"hash":"123","id":456}

我想缩进字符串并将其序列化为JSON文件。简单地说,我可以使用Newtonsoft缩进字符串,如下所示。

using Newtonsoft.Json.Linq;
JToken token = JToken.Parse(json);
var formattedJson = JObject.Parse(token.ToString()).ToString();

然而,由于我使用了相当多的大型JSON对象,我主要对可以对数据流进行操作的解决方案感兴趣。出于性能方面的原因,我决定使用System.Text.Json,并且我想知道它是否带有任何用于处理数据流的开箱即用功能。

在推出我自己的解决方案之前,我想知道是否有任何方法可以实现开箱即用的功能,理想情况下是在将输入流写入存储时拦截输入流(即动态转换(。或者,我可以处理一个序列化流,但这需要读取文件,进行必要的更改,并写入输出文件,而不需要首先将整个JSON反序列化到内存中。我主要对第一种方法感兴趣,因为(a(我将遍历JSON一次,(b(这不需要存储中间文件(流->未格式化的JSON->格式化的JSON(。

动机

上游服务正在以JSON格式流式传输大量信息。下游服务以逐行的方式读取JSON并提取所需的字段;可能是由于JSON文件的大尺寸,使得它们在内存中反序列化不切实际。然而,为了使流式JSON与下游服务兼容,需要对其进行一些约定。其中一个必需的约定是缩进和每行一个键值对。上游服务似乎放弃了所有的格式化以减少比特流,但下游服务依赖于格式化来提取信息。上游和下游服务都超出了我的控制范围。我编写的服务的目标是坐在中间,对流式JSON进行必要的约定(缩进等格式就是其中之一(,使其与下游服务兼容。

如上所述,将流式JSON反序列化为对象,进行必要的更改,并将更新后的JSON序列化到磁盘,似乎是一个显而易见的解决方案,然而,考虑到数据的大小和数量,这种方法对我的应用程序来说是不切实际/不可行的。

我可以想到一个中间层,它动态处理流式JSON,并在将位写入持久性介质之前进行更改。然而,在走这条路之前,我想仔细检查System.Text.Json中是否有任何现成的功能来处理信息流。

更新

这个问题在很大程度上是为了清晰和强调要点而更新的:System.Text.Json中是否有用于处理JSON流的开箱即用功能?

使用System.Text.Json似乎没有简单的方法,因为您不能直接将Stream对象与System.Text.Json.Utf8JsonReader一起使用。为了绕过这个限制,您需要使用System.Text.Json.JsonDocument对象将文件内容放入内存中,因此很明显,它会占用大量内存。

目前,对于网络上的阅读,唯一能提高内存效率的解决方案是使用Newtonsoft.Json库。

using (var streamReader = new StreamReader(sourceFilePath))
using (var jsonTextReader = new JsonTextReader(streamReader))
using (var streamWriter = File.CreateText(destinationFilePath))
using (var jsonTextWriter = new JsonTextWriter(streamWriter))
{
jsonTextWriter.Formatting = Formatting.Indented;
while (jsonTextReader.Read())
{
jsonTextWriter.WriteToken(jsonTextReader);
}
}

使用1MB缓冲区而不是默认的4kB缓冲区会更快。在5m05秒内将6.33 GB的文件缩进13.7 GB,总共约200.000000行。它在HDD上读写,在Visual Studio(调试版(中运行,只使用17MB RAM。由于空间限制,无法在SSD上进行测试。

string filename = @"VeryBig.json";
using FileStream inputFileStream = new(filename, FileMode.Open, FileAccess.Read, FileShare.Read, 1 * 1024 * 1024);
using StreamReader streamReader = new(inputFileStream);
using JsonTextReader jsonTextReader = new(streamReader);
string filenameOutput = Path.ChangeExtension(filename, ".indented.json");
using FileStream outputFileStream = new(filenameOutput, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, 1 * 1024 * 1024);
using StreamWriter streamWriter = new(outputFileStream);
using JsonTextWriter jsonTextWriter = new(streamWriter);
jsonTextWriter.Formatting = Formatting.Indented;
while (jsonTextReader.Read())
{
jsonTextWriter.WriteToken(jsonTextReader);
}

我刚刚测试过,没有发现任何问题

var json="{"hash":"123","id":456}";
var jsonObject=JsonDocument.Parse(json);
json =  System.Text.Json.JsonSerializer.Serialize(jsonObject, 
new JsonSerializerOptions() { WriteIndented = true });

测试结果

{
"hash": "123",
"id": 456
}

使用System.Text.Json和好友的简单方法:

using System;
using System.Text;
using System.Text.Json;
using System.IO;

public class Program
{
const string template = @"
Original:
---------
{0}
---------
Pretty:
---------
{1}
---------
";
public static void Main()
{
var src  = "{"hash":"123","id":456}";

using ( var doc = JsonDocument.Parse(src, new JsonDocumentOptions{ AllowTrailingCommas = true }) )
using ( var ms  = new MemoryStream() )
using ( var jsonWriter = new Utf8JsonWriter( ms, new JsonWriterOptions{ Indented = true } ) )
{

doc.RootElement.WriteTo(jsonWriter);

jsonWriter.Flush();
ms.Flush();

string pretty = Encoding.UTF8.GetString(ms.ToArray());

Console.WriteLine( template , src, pretty );

}

}
}

产生预期的

Original:
---------
{"hash":"123","id":456}
---------
Pretty:
---------
{
"hash": "123",
"id": 456
}
---------

System.Text.Json中是否有任何现成的功能用于处理Json流?

是。。。有点。

您可以使用Utf8JsonReader从流中读取。但它本身并不支持流:它依赖于ReadOnlySequence<>s。因此,使用它进行流传输的代码相当粗糙,需要创建自己的缓冲区并自己从流中读取。

最新更新