.net 如何反序列化巨大的JSON成员

63lcw9qa  于 2023-08-08  发布在  .NET
关注(0)|答案(1)|浏览(103)

我正在调用一个API,它将响应作为JSON对象返回。JSON对象的一个成员可以有一个 really long(10 MiB到3GiB+)base-64编码值。举例来说:

{
    "name0": "value0",
    "name1": "value1",
    "data": "(very very long base-64 value here)",
    "name2": "value2"
}

字符串
我需要数据和其他名称/值从身体。我如何获得这些数据?
我目前正在使用Newtonsoft.json在这个应用程序中序列化JSON数据,对于较小的数据块,我通常会有一个byte[]类型的Data属性,但这个数据可能超过2GiB,即使它小于这个值,也可能会有太多的响应返回,以至于我们可能会耗尽内存。
我希望有一种方法可以编写一个自定义的JsonConverter或其他东西来将数据序列化/反序列化为System.IO.Stream,但我不确定如何读取一个本身无法放入内存的字符串“令牌”。有什么建议吗?

ghg1uchk

ghg1uchk1#

3GiB+字符串值太大,不适合.NET字符串,因为它将超过maximum .NET string length。因此,您不能使用Json.NET读取JSON响应,因为Json.NET的JsonTextReader在读取even when skipping then时总是完全物化属性值。
至于反序列化为Streambyte []数组,如Panagiotis Kanavos的注解中所述
JSON.NET的JsonTextReader和System.Text.Json的Utf8 JsonReader都没有将节点作为流检索的方法。所有与字节相关的方法一次返回全部内容。
因此,对于足够大的data值,您将超过maximum .NET array length
你有什么选择

    • JSON并不是一种理想的格式,因为大多数JSON序列化器将完全具体化每个属性。相反,正如Panagiotis Kanavos所建议的那样,在响应正文中发送二进制数据,并将其余属性作为自定义头发送。或查看 * HTTP response with both binary data and JSON * 以了解其他选项。如果你这样做,你将能够直接从响应主体流复制到一些中间流。
      其次,您可以尝试将代码从this answer通过mtosh推广到 * Parsing a JSON file with .NET core 3.0/System.text.Json *。这个答案展示了如何使用System.Text.JSON中的Utf8JsonReader逐个令牌地迭代流。您可以尝试重写该答案,以支持增量阅读单个字符串值-但是,我必须承认,我不知道Utf8JsonReader是否真的支持在不加载整个值的情况下以块的形式读取属性值的一部分。因此,我不能推荐这种方法。
      第三,您可以采用this answer到 * JsonConvert Deserialize Object out of memory exception * 的方式,使用JsonReaderWriterFactory.CreateJsonReader()返回的reader手动解析您的JSON。这个工厂返回一个XmlDictionaryReader,它动态地将JSON代码转换为XML,因此支持通过XmlReader.ReadContentAsBase64(Byte[], Int32, Int32)增量阅读Base64属性。这是WCF的DataContractJsonSerializer使用的读取器,不建议用于新的开发,但已移植到.NET Core,因此可以在没有其他选项时使用。

那么,这将如何工作?首先定义一个与JSON对应的模型,如下所示,将Data属性表示为Stream

public partial class Model : IDisposable
{
    Stream data;

    public string Name0 { get; set; }
    public string Name1 { get; set; }
    [System.Text.Json.Serialization.JsonIgnore] // Added for debugging purposes
    public Stream Data { get => data; set => this.data = value; }
    public string Name2 { get; set; }
    
    public virtual void Dispose() => Interlocked.Exchange(ref data, null)?.Dispose();
}

字符串
接下来,定义以下扩展方法:

public class JsonReaderWriterExtensions
{
    const int BufferSize = 8192;
    private static readonly Microsoft.IO.RecyclableMemoryStreamManager manager = new ();

    public static Stream CreateTemporaryStream() => 
        // Create some temporary stream to hold the deserialized binary data.  
        // Could be a FileStream created with FileOptions.DeleteOnClose or a Microsoft.IO.RecyclableMemoryStream
        // File.Create(Path.GetTempFileName(), BufferSize, FileOptions.DeleteOnClose);
        manager.GetStream();
    
    public static T DeserializeModelWithStreams<T>(Stream inputStream) where T : new() =>
        PopulateModelWithStreams(inputStream, new T());

    public static T PopulateModelWithStreams<T>(Stream inputStream, T model)
    {
        ArgumentNullException.ThrowIfNull(inputStream);
        ArgumentNullException.ThrowIfNull(model);

        var type = model.GetType();
        
        using (var reader = JsonReaderWriterFactory.CreateJsonReader(inputStream, XmlDictionaryReaderQuotas.Max))
        {
            // TODO: Stream-valued properties not at the root level.
            if (reader.MoveToContent() != XmlNodeType.Element)
                throw new XmlException();
            while (reader.Read() && reader.NodeType != XmlNodeType.EndElement)
            {
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        var name = reader.LocalName;
                        // TODO:
                        // Here we could use use DataMemberAttribute.Name or other attributes to build a contract mapping the type to the JSON.
                        var property = type.GetProperty(name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                        if (property == null || !property.CanWrite || property.GetIndexParameters().Length > 0 || Attribute.IsDefined(property, typeof(IgnoreDataMemberAttribute)))
                            continue;
                        // Deserialize the value
                        using (var subReader = reader.ReadSubtree())
                        {
                            subReader.MoveToContent();
                            if (typeof(Stream).IsAssignableFrom(property.PropertyType))
                            {
                                var streamValue = CreateTemporaryStream();  
                                byte[] buffer = new byte[BufferSize];
                                int readBytes = 0;
                                while ((readBytes = subReader.ReadElementContentAsBase64(buffer, 0, buffer.Length)) > 0)
                                    streamValue.Write(buffer, 0, readBytes);
                                if (streamValue.CanSeek)
                                    streamValue.Position = 0;
                                property.SetValue(model, streamValue);
                            }
                            else
                            {
                                var settings = new DataContractJsonSerializerSettings
                                {
                                    RootName = name,
                                    // Modify other settings as required e.g. DateTimeFormat.
                                };
                                var serializer = new DataContractJsonSerializer(property.PropertyType, settings);
                                var value = serializer.ReadObject(subReader);
                                if (value != null)
                                    property.SetValue(model, value);
                            }
                        }
                        Debug.Assert(reader.NodeType == XmlNodeType.EndElement);
                        break;
                    default:
                        reader.Skip();
                        break;
                }
            }
        }

        return model;
    }
}


现在你可以反序列化你的模型如下:

using var model = JsonReaderWriterExtensions.DeserializeModelWithStreams<Model>(responseStream);


备注:

  • 由于data的值可以任意大,因此无法将其内容反序列化为MemoryStream。替代方案包括:
  • 临时FileStream,例如由File.Create(Path.GetTempFileName(), BufferSize, FileOptions.DeleteOnClose)返回。
  • MSFT的Microsoft.IO.RecyclableMemoryStream nuget包返回的RecyclableMemoryStream

上面的演示代码使用RecyclableMemoryStream,但如果您愿意,可以将其更改为使用FileStream。无论哪种方式,你都需要在完成后处理它。

  • 我使用反射将c#属性通过名称绑定到JSON属性,忽略大小写。对于值类型不是Stream的属性,我使用DataContractJsonSerializer来反序列化它们的值。这个序列化器有很多怪癖,比如一个时髦的默认DateTime,所以你可能需要尝试一下你的DataContractJsonSerializerSettings,或者手动反序列化某些属性。
  • 我的方法JsonReaderWriterExtensions.DeserializeModelWithStreams()只支持根级别的Stream值属性。如果你嵌套了巨大的Base64值属性,你将需要重写JsonReaderWriterExtensions.PopulateModelWithStreams()以使其递归(这基本上相当于编写你自己的序列化器)。
  • 有关JsonReaderWriterFactory返回的读取器如何将JSON转换为XML的讨论,请参阅 * Efficiently replacing properties of a large JSON using System.Text.Json * 和 * Mapping Between JSON and XML *。

演示小提琴here

相关问题