如何使用 System.Text.Json 将 Newtonsoft JToken 序列化为 JSON?
How can I serialize a Newtonsoft JToken to JSON using System.Text.Json?
在升级到ASP.NETCore 5的过程中,遇到了需要序列化和return一个Json.NETJObject
(returned by some legacy code we can't yet change) using System.Text.Json
. How can this be done in a reasonably efficient manner, without re-serializing and re-parsing the JSON to a JsonDocument
or reverting back to Json.NET completely via ?[=24的情况=]
具体来说,假设我们有以下遗留数据模型:
public class Model
{
public JObject Data { get; set; }
}
当我们从 ASP.NET Core 5.0 中 return 时,“值”属性 的内容被破坏成一系列空数组。例如:
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":[[[],[],[],[],[],[],[],[],[],[]]]}}
如何正确地将 JObject
属性 序列化为 System.Text.Json
?请注意,JObject
可能相当大,因此我们更愿意将其流式传输,而不是将其格式化为字符串,然后从头开始再次将其解析为 JsonDocument
简单地 return。
需要创建一个 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
可能会在反序列化期间传递给自定义设置,例如 FloatParseHandling
或 DateFormatHandling
。
要将 JTokenConverterFactory
添加到 ASP.NET 核心序列化选项,请参阅 Configure System.Text.Json-based formatters。
演示 fiddle 以及一些基本测试:fiddle #1。
可以在此处找到通过从 Utf8JsonReader
流式传输到 JsonWriter
而无需将整个 JSON 值加载到 JsonDocument
来实现反序列化的原型版本:fiddle #2.
在升级到ASP.NETCore 5的过程中,遇到了需要序列化和return一个Json.NET 具体来说,假设我们有以下遗留数据模型: 当我们从 ASP.NET Core 5.0 中 return 时,“值”属性 的内容被破坏成一系列空数组。例如: 失败,并生成以下不正确的 JSON: 如何正确地将 JObject
(returned by some legacy code we can't yet change) using System.Text.Json
. How can this be done in a reasonably efficient manner, without re-serializing and re-parsing the JSON to a JsonDocument
or reverting back to Json.NET completely via public class Model
{
public JObject Data { get; set; }
}
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)]));
{"Data":{"value":[[[],[],[],[],[],[],[],[],[],[]]]}}
JObject
属性 序列化为 System.Text.Json
?请注意,JObject
可能相当大,因此我们更愿意将其流式传输,而不是将其格式化为字符串,然后从头开始再次将其解析为 JsonDocument
简单地 return。
需要创建一个 custom JsonConverterFactory
以使用 System.Text.Json
.
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
可能会在反序列化期间传递给自定义设置,例如FloatParseHandling
或DateFormatHandling
。要将
JTokenConverterFactory
添加到 ASP.NET 核心序列化选项,请参阅 Configure System.Text.Json-based formatters。
演示 fiddle 以及一些基本测试:fiddle #1。
可以在此处找到通过从 Utf8JsonReader
流式传输到 JsonWriter
而无需将整个 JSON 值加载到 JsonDocument
来实现反序列化的原型版本:fiddle #2.