如何使用 System.Text.Json 将 Newtonsoft JToken 序列化为 JSON?

dbc*_*dbc 6 c# json json.net system.text.json asp.net-core-5.0

在升级到 ASP.NET Core 5 的过程中,我们遇到了一种情况,需要JObject使用System.Text.Json. 如何以相当有效的方式完成此操作,而不需要将 JSON 重新序列化和重新解析为 aJsonDocument或通过 完全恢复回 Json.NET AddNewtonsoftJson()

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

public class Model
{
    public JObject Data { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

当我们从 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)]));
Run Code Online (Sandbox Code Playgroud)

失败,并生成以下错误的 JSON:

public class Model
{
    public JObject Data { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

dbc*_*dbc 10

有必要创建一个自定义JsonConverterFactoryJToken来使用 .NET 将Json.NET 层次结构序列化为JSON System.Text.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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

var options = new JsonSerializerOptions
{
    Converters = { new JTokenConverterFactory() },
};
var outputJson = JsonSerializer.Serialize(model, options);
Run Code Online (Sandbox Code Playgroud)

笔记:

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

  • 可以传递NewtonsoftJsonSerializerSettings来自定义设置,例如反序列化FloatParseHandlingDateFormatHandling反序列化期间。

  • 要添加JTokenConverterFactoryASP.NET Core 序列化选项,请参阅配置基于 System.Text.Json 的格式化程序

此处演示了一些基本测试:fiddle #1

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