在升级到 ASP.NET Core 5 的过程中,我们遇到了一种情况,需要使用 JObject
序列化并返回一个 Json.NET
System.Text.Json
(由一些我们还无法更改的遗留代码返回)。 如何以相当有效的方式完成此操作,而不需要将 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":[[[],[],[],[],[],[],[],[],[],[]]]}}
如何使用
JObject
正确序列化 System.Text.Json
属性? 请注意,JObject
可能相当大,因此我们更愿意将其流式传输,而不是将其格式化为字符串,然后从头开始再次将其解析为JsonDocument
以简单地返回它。
需要创建一个 自定义
JsonConverterFactory
才能使用 JToken
将 Json.NET System.Text.Json
层次结构序列化为 JSON。
由于问题旨在避免将整个
JObject
重新序列化为 JSON,只是为了使用 System.Text.Json
再次解析它,因此以下转换器沿着令牌层次结构下降,递归地将每个单独的值写入 Utf8JsonWriter
:
using System.Text.Json;
using System.Text.Json.Serialization;
using Newtonsoft.Json.Linq;
public class JTokenConverterFactory : JsonConverterFactory
{
// Cache well known converters to avoid problems in Native AOT mode
static readonly IReadOnlyDictionary<Type, Func<JsonSerializerOptions, Newtonsoft.Json.JsonSerializerSettings?, JsonConverter>> WellKnownConverterFactories =
new Dictionary<Type, Func<JsonSerializerOptions, Newtonsoft.Json.JsonSerializerSettings?, JsonConverter>>()
{
[typeof(JToken)] = (options, settings) => new JTokenConverter<JToken>(options, settings),
[typeof(JValue)] = (options, settings) => new JTokenConverter<JValue>(options, settings),
[typeof(JRaw)] = (options, settings) => new JTokenConverter<JRaw>(options, settings),
[typeof(JContainer)] = (options, settings) => new JTokenConverter<JContainer>(options, settings),
[typeof(JObject)] = (options, settings) => new JTokenConverter<JObject>(options, settings),
[typeof(JArray)] = (options, settings) => new JTokenConverter<JArray>(options, settings),
[typeof(JConstructor)] = (options, settings) => throw new JsonException("Serialization of non-standard JConstructor token with System.Text.Json is not supported."),
[typeof(JProperty)] = (options, settings) => throw new JsonException("JProperty cannot be serialized from or to JSON as a standalone object."),
};
// In case you need to set FloatParseHandling or DateFormatHandling
readonly Newtonsoft.Json.JsonSerializerSettings? settings;
public JTokenConverterFactory(Newtonsoft.Json.JsonSerializerSettings? settings) => this.settings = settings;
public JTokenConverterFactory() : this(null) { }
public override bool CanConvert(Type typeToConvert) => typeof(JToken).IsAssignableFrom(typeToConvert);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (WellKnownConverterFactories.TryGetValue(typeToConvert, out var factory))
return factory(options, settings);
// All known JToken types (as of Json.NET 13.0) are included in WellKnownConverterFactories, the below call to MakeGenericType() is future-proofing.
// So, in native AOT mode, one could remove the MakeGenericType() call and just do:
// throw new JsonException($"Unknown JToken type {typeToConvert}");
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 Newtonsoft.Json.JsonSerializerSettings? settings;
public JTokenConverter(JsonSerializerOptions options, Newtonsoft.Json.JsonSerializerSettings? settings) => this.settings = settings;
public override bool CanConvert(Type typeToConvert) => typeof(TJToken).IsAssignableFrom(typeToConvert);
public override TJToken? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var newtonsoftWriter = new JTokenWriter();
ReadCore(ref reader, newtonsoftWriter, options, typeof(TJToken), Newtonsoft.Json.JsonSerializer.CreateDefault(settings));
return (TJToken?)newtonsoftWriter.Token;
}
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);
static 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 null:
writer.WriteNullValue();
break;
case JValue jvalue when jvalue.Value is bool v:
writer.WriteBooleanValue(v);
break;
case JValue jvalue when jvalue.Value is string v:
writer.WriteStringValue(v);
break;
case JValue jvalue when jvalue.Value is long v:
writer.WriteNumberValue(v);
break;
case JValue jvalue when jvalue.Value is int v:
writer.WriteNumberValue(v);
break;
case JValue jvalue when jvalue.Value is decimal v:
writer.WriteNumberValue(v);
break;
case JValue jvalue when jvalue.Value is double v:
writer.WriteNumberValue(v);
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;
}
}
static void ReadArrayCore(ref Utf8JsonReader reader, Newtonsoft.Json.JsonWriter newtonsoftWriter, JsonSerializerOptions options, Newtonsoft.Json.JsonSerializer newtonsoftSerializer)
{
if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException();
newtonsoftWriter.WriteStartArray();
while (reader.Read())
{
switch (reader.TokenType)
{
default:
ReadCore(ref reader, newtonsoftWriter, options, typeof(JToken), newtonsoftSerializer);
break;
case JsonTokenType.EndArray:
newtonsoftWriter.WriteEndArray();
return;
}
}
throw new JsonException();
}
static void ReadObjectCore(ref Utf8JsonReader reader, Newtonsoft.Json.JsonWriter newtonsoftWriter, JsonSerializerOptions options, Newtonsoft.Json.JsonSerializer newtonsoftSerializer)
{
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
newtonsoftWriter.WriteStartObject();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
var name = reader.GetString()!;
if (!reader.Read())
throw new JsonException();
newtonsoftWriter.WritePropertyName(name);
ReadCore(ref reader, newtonsoftWriter, options, typeof(JToken), newtonsoftSerializer);
break;
case JsonTokenType.EndObject:
newtonsoftWriter.WriteEndObject();
return;
default:
throw new JsonException();
}
}
throw new JsonException();
}
static void CheckType(Type extectedType, Type actualType, JsonTokenType tokenType)
{
if (!extectedType.IsAssignableFrom(actualType))
throw new JsonException(string.Format("Expected type {0} cannot be created from token {1}", extectedType, tokenType));
}
static void ReadCore(ref Utf8JsonReader reader, Newtonsoft.Json.JsonWriter newtonsoftWriter, JsonSerializerOptions options, Type extectedType, Newtonsoft.Json.JsonSerializer newtonsoftSerializer)
{
var t = reader.TokenType;
switch (reader.TokenType)
{
case JsonTokenType.Comment:
CheckType(extectedType, typeof(JValue), reader.TokenType);
newtonsoftWriter.WriteComment(reader.GetString());
break;
case JsonTokenType.False:
CheckType(extectedType, typeof(JValue), reader.TokenType);
newtonsoftWriter.WriteValue(false);
break;
case JsonTokenType.True:
CheckType(extectedType, typeof(JValue), reader.TokenType);
newtonsoftWriter.WriteValue(true);
break;
case JsonTokenType.Null:
CheckType(extectedType, typeof(JValue), reader.TokenType);
newtonsoftWriter.WriteNull();
break;
case JsonTokenType.String:
{
// To ensure that DateTime values are recognized consistently with Json.NET conventions, we must invoke the Json.NET
// serializer or parser using the incoming JsonSerializerSettings. See https://www.newtonsoft.com/json/help/html/DatesInJSON.htm
CheckType(extectedType, typeof(JValue), reader.TokenType);
using var newtonsoftReader = new Newtonsoft.Json.JsonTextReader(new StringReader(Newtonsoft.Json.JsonConvert.ToString(reader.GetString())));
newtonsoftSerializer.Deserialize<JValue>(newtonsoftReader)!.WriteTo(newtonsoftWriter);
}
break;
case JsonTokenType.Number:
CheckType(extectedType, typeof(JValue), reader.TokenType);
// For efficiency, see if the value is an integer convertible to long.
if (reader.TryGetInt64(out var l))
newtonsoftWriter.WriteValue(l);
else
{
// Inconsistencies in numeric formats cause difficulties here. System.Text.Json keeps the underlying byte representation while JsonReader parses
// to the closest fit .Net numeric type (long, double, decimal or BigInteger). Just get the raw JSON and let Newtonsoft handle the conversion.
using var doc = JsonDocument.ParseValue(ref reader);
using var newtonsoftReader = new Newtonsoft.Json.JsonTextReader(new StringReader(doc.RootElement.ToString()));
newtonsoftSerializer.Deserialize<JValue>(newtonsoftReader)!.WriteTo(newtonsoftWriter);
}
break;
case JsonTokenType.StartArray:
CheckType(extectedType, typeof(JArray), reader.TokenType);
ReadArrayCore(ref reader, newtonsoftWriter, options, newtonsoftSerializer);
break;
case JsonTokenType.StartObject:
CheckType(extectedType, typeof(JObject), reader.TokenType);
ReadObjectCore(ref reader, newtonsoftWriter, options, newtonsoftSerializer);
break;
}
}
}
}
那么问题中的单元测试应该修改为使用以下
JsonSerializerOptions
:
var options = new JsonSerializerOptions
{
Converters = { new JTokenConverterFactory() },
};
var outputJson = JsonSerializer.Serialize(model, options);
备注:
转换器实现了
JToken
类型的流序列化和反序列化。
JsonSerializerSettings
可以在反序列化过程中传递自定义设置,例如 FloatParseHandling
或 DateFormatHandling
。
要将
JTokenConverterFactory
添加到 ASP.NET Core 序列化选项,请参阅配置基于 System.Text.Json 的格式化程序。
此处演示了一些基本测试:https://dotnetfiddle.net/KFJjH4。