【问题标题】:How can I serialize a Newtonsoft JToken to JSON using System.Text.Json?如何使用 System.Text.Json 将 Newtonsoft JToken 序列化为 JSON?
【发布时间】:2021-03-30 19:43:51
【问题描述】:

在升级到 ASP.NET Core 5 的过程中,我们遇到了需要使用@987654325 序列化并返回一个Json.NET JObject(由一些我们还不能更改的遗留代码返回)的情况@。如何以合理有效的方式完成此操作,而不需要将 JSON 重新序列化和重新解析为 JsonDocument 或通过 AddNewtonsoftJson() 完全恢复为 Json.NET?

具体来说,假设我们有以下遗留数据模型:

public class Model
{
    public JObject Data { get; set; }
}

当我们从 ASP.NET Core 5.0 返回它时,“value”属性的内容会变成一系列空数组。例如:

var inputJson = @"{""value"":[[null,true,false,1010101,1010101.10101,""hello"",""????"",""\uD867\uDE3D"",""2009-02-15T00:00:00Z"",""\uD867\uDE3D\u0022\\/\b\f\n\r\t\u0121""]]}";
var model = new Model { Data = JObject.Parse(inputJson) };
var outputJson = JsonSerializer.Serialize(model);

Console.WriteLine(outputJson);

Assert.IsTrue(JToken.DeepEquals(JToken.Parse(inputJson), JToken.Parse(outputJson)[nameof(Model.Data)]));

失败,并生成以下不正确的 JSON:

{"Data":{"value":[[[],[],[],[],[],[],[],[],[],[]]]}}

如何使用System.Text.Json 正确序列化JObject 属性?请注意,JObject 可能相当大,因此我们更愿意将其流式传输,而不是将其格式化为字符串,然后从头开始将其重新解析为 JsonDocument,然后将其返回。

【问题讨论】:

标签: c# json json.net system.text.json asp.net-core-5.0


【解决方案1】:

需要创建一个custom JsonConverterFactory 以使用System.Text.Json 将Json.NET JToken 层次结构序列化为JSON。

由于该问题旨在避免将整个 JObject 重新序列化为 JSON 以再次使用 System.Text.Json 对其进行解析,因此以下转换器会降低令牌层次结构,递归地将每个单独的值写入 Utf8JsonWriter

using System.Text.Json;
using System.Text.Json.Serialization;
using Newtonsoft.Json.Linq;

public class JTokenConverterFactory : JsonConverterFactory
{
    // In case you need to set FloatParseHandling or DateFormatHandling
    readonly Newtonsoft.Json.JsonSerializerSettings settings;
    
    public JTokenConverterFactory() { }

    public JTokenConverterFactory(Newtonsoft.Json.JsonSerializerSettings settings) => this.settings = settings;

    public override bool CanConvert(Type typeToConvert) => typeof(JToken).IsAssignableFrom(typeToConvert);

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        var converterType = typeof(JTokenConverter<>).MakeGenericType(new [] { typeToConvert} );
        return (JsonConverter)Activator.CreateInstance(converterType, new object [] { options, settings } );
    }

    class JTokenConverter<TJToken> : JsonConverter<TJToken> where TJToken : JToken
    {
        readonly JsonConverter<bool> boolConverter;
        readonly JsonConverter<long> longConverter;
        readonly JsonConverter<double> doubleConverter;
        readonly JsonConverter<decimal> decimalConverter;
        readonly JsonConverter<string> stringConverter;
        readonly JsonConverter<DateTime> dateTimeConverter;
        readonly Newtonsoft.Json.JsonSerializerSettings settings;

        public override bool CanConvert(Type typeToConvert) => typeof(TJToken).IsAssignableFrom(typeToConvert);

        public JTokenConverter(JsonSerializerOptions options, Newtonsoft.Json.JsonSerializerSettings settings)
        {
            // Cache some converters for efficiency
            boolConverter = (JsonConverter<bool>)options.GetConverter(typeof(bool));
            stringConverter = (JsonConverter<string>)options.GetConverter(typeof(string));
            longConverter = (JsonConverter<long>)options.GetConverter(typeof(long));
            decimalConverter = (JsonConverter<decimal>)options.GetConverter(typeof(decimal));
            doubleConverter = (JsonConverter<double>)options.GetConverter(typeof(double));
            dateTimeConverter = (JsonConverter<DateTime>)options.GetConverter(typeof(DateTime));
            this.settings = settings;
        }

        public override TJToken Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            // This could be substantially optimized for memory use by creating code to read from a Utf8JsonReader and write to a JsonWriter (specifically a JTokenWriter).
            // We could just write the JsonDocument to a string, but System.Text.Json works more efficiently with UTF8 byte streams so write to one of those instead.
            using var doc = JsonDocument.ParseValue(ref reader);
            using var ms = new MemoryStream();
            using (var writer = new Utf8JsonWriter(ms))
                doc.WriteTo(writer);
            ms.Position = 0;
            using (var sw = new StreamReader(ms))
            using (var jw = new Newtonsoft.Json.JsonTextReader(sw))
            {
                return Newtonsoft.Json.JsonSerializer.CreateDefault(settings).Deserialize<TJToken>(jw);
            }
        }

        public override void Write(Utf8JsonWriter writer, TJToken value, JsonSerializerOptions options) =>
            // Optimize for memory use by descending the JToken hierarchy and writing each one out, rather than formatting to a string, parsing to a `JsonDocument`, then writing that.
            WriteCore(writer, value, options);

        void WriteCore(Utf8JsonWriter writer, JToken value, JsonSerializerOptions options)
        {
            if (value == null || value.Type == JTokenType.Null)
            {
                writer.WriteNullValue();
                return;
            }

            switch (value)
            {
                case JValue jvalue when jvalue.GetType() != typeof(JValue): // JRaw, maybe others
                default: // etc
                    {
                        // We could just format the JToken to a string, but System.Text.Json works more efficiently with UTF8 byte streams so write to one of those instead.
                        using var ms = new MemoryStream();
                        using (var tw = new StreamWriter(ms, leaveOpen : true))
                        using (var jw = new Newtonsoft.Json.JsonTextWriter(tw))
                        {
                            value.WriteTo(jw);
                        }
                        ms.Position = 0;
                        using var doc = JsonDocument.Parse(ms);
                        doc.WriteTo(writer);
                    }
                    break;
                // Hardcode some standard cases for efficiency
                case JValue jvalue when jvalue.Value is bool v:
                    boolConverter.WriteOrSerialize(writer, v, options);
                    break;
                case JValue jvalue when jvalue.Value is string v:
                    stringConverter.WriteOrSerialize(writer, v, options);
                    break;
                case JValue jvalue when jvalue.Value is long v:
                    longConverter.WriteOrSerialize(writer, v, options);
                    break;
                case JValue jvalue when jvalue.Value is decimal v:
                    decimalConverter.WriteOrSerialize(writer, v, options);
                    break;
                case JValue jvalue when jvalue.Value is double v:
                    doubleConverter.WriteOrSerialize(writer, v, options);
                    break;
                case JValue jvalue when jvalue.Value is DateTime v:
                    dateTimeConverter.WriteOrSerialize(writer, v, options);
                    break;
                case JValue jvalue:
                    JsonSerializer.Serialize(writer, jvalue.Value, options);
                    break;
                case JArray array:
                    {
                        writer.WriteStartArray();
                        foreach (var item in array)
                            WriteCore(writer, item, options);
                        writer.WriteEndArray();
                    }
                    break;
                case JObject obj:
                    {
                        writer.WriteStartObject();
                        foreach (var p in obj.Properties())
                        {
                            writer.WritePropertyName(p.Name);
                            WriteCore(writer, p.Value, options);
                        }
                        writer.WriteEndObject();
                    }
                    break;
            }
        }
    }
}

public static class JsonExtensions
{
    public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        if (converter != null)
            converter.Write(writer, value, options);
        else
            JsonSerializer.Serialize(writer, value, options);
    }
}

那么问题中的单元测试应该修改为使用以下JsonSerializerOptions

var options = new JsonSerializerOptions
{
    Converters = { new JTokenConverterFactory() },
};
var outputJson = JsonSerializer.Serialize(model, options);

注意事项:

  • 转换器实现JToken 类型的反序列化以及序列化,但是由于这不是问题的严格要求,它只是将整个 JSON 层次结构读入JsonDocument,然后将其输出到MemoryStream并使用 Json.NET 重新解析它。

  • Newtonsoft 的JsonSerializerSettings 可以在反序列化期间传递给自定义设置,例如FloatParseHandlingDateFormatHandling

  • 要将JTokenConverterFactory 添加到 ASP.NET Core 序列化选项,请参阅Configure System.Text.Json-based formatters

在这里演示一些基本测试:fiddle #1

可以在此处找到通过从 Utf8JsonReader 流式传输到 JsonWriter 而无需将整个 JSON 值加载到 JsonDocument 来实现反序列化的原型版本:fiddle #2

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-24
    • 2023-03-18
    • 1970-01-01
    • 2021-08-11
    • 1970-01-01
    相关资源
    最近更新 更多