在 System.Text.Json 序列化中添加 $type,就像 Newtonsoft 处理动态对象属性一样
本文介绍了一种使用 System.Text.Json JsonSerializer 序列化包含动态类型的模型的技术,该技术不支持 $type。
引言
欢迎阅读我为 C# 开发者撰写的新文章。今天,我想讨论 JSON 序列化。最近,Microsoft 将 WEB API 的默认序列化从 Newtonsoft JsonConvert
更改为 System.Text.Json JsonSerializer
,开发者们发现一个重要的功能不再受支持。我指的是 Newtonsoft JsonConvert
在对象序列化时可以为每个复杂类型添加的 “$type”
属性,并使用它来反序列化回对象。
如果您使用以下序列化设置
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = Newtonsoft.Json.TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
};
并尝试序列化在 Model
属性中包含 MyModel
对象的 MyState
对象
public class MyModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class MyState
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsReady { get; set; }
public DateTime LastUpdated { get; set; }
public object Model { get; set; }
}
Newtonsoft JsonConvert
将创建一个带有上述 “$type”
属性的 JSON 对象
{
"$type": "DemoSystemTextJson.Tests.MyState, DemoSystemTextJson.Tests",
"Id": 11,
"Name": "CurrentState",
"IsReady": true,
"LastUpdated": "2015-10-21T00:00:00",
"Model": {
"$type": "DemoSystemTextJson.Tests.MyModel, DemoSystemTextJson.Tests",
"FirstName": "Alex",
"LastName": "Brown",
"BirthDate": "1990-01-12T00:00:00"
}
}
正如您所见,已添加 "$type"
属性,并用于在反序列化过程中帮助识别类型。
此外,需要注意的是,"$type"
属性位于每个对象的第一个位置,否则 Newtonsoft JsonConvert 将无法识别它。
有处理 PostgreSQL 经验的开发者可能注意到,当您在 Postgres 数据库中存储 JSON 并将其读回时,属性的顺序将不再是之前的顺序,而是以某种方式排序——这是因为 Postgres 出于内部优化目的将 JSON 对象存储为键值对。您可以从 Postgres 读取 JSON 时获得此 JSON
{
"Id": 11,
"Name": "CurrentState",
"IsReady": true,
"LastUpdated": "2015-10-21T00:00:00",
"Model": {
"FirstName": "Alex",
"LastName": "Brown",
"BirthDate": "1990-01-12T00:00:00",
"$type": "DemoSystemTextJson.Tests.MyModel, DemoSystemTextJson.Tests"
},
"$type": "DemoSystemTextJson.Tests.MyState, DemoSystemTextJson.Tests"
}
而 Newtonsoft JsonConvert 将无法识别它。
为了处理 WEB API 和 PostgreSQL,我们将使用 System.Text.Json
JsonSerializer 和一些真正的程序员可能会添加到代码中的“魔法”,让我们创建一个用户故事。
用户故事 #5:在 System.Text.Json JsonSerializer 中支持动态类型
- 创建一个类,允许序列化和反序列化包含未知类型属性的对象
- JSON 属性的顺序不应影响反序列化过程
演示项目和测试
为了开始实现用户故事,我将创建一个 DemoSystemTextJson
类库(.NET Core),并添加一个 xUnit 测试项目(.NET Core)- DemoSystemTextJson.Tests
。
我更喜欢从编写测试开始,最初,我们需要有将要序列化和反序列化的 Model
类,让我们在测试项目中添加它们
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson.Tests
{
public class MyModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class MyState
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsReady { get; set; }
public DateTime LastUpdated { get; set; }
public object Model { get; set; }
}
}
有了这些类,我们就可以创建第一个测试来检查是否可以立即反序列化 MyState
using System;
using Xunit;
using System.Text.Json;
namespace DemoSystemTextJson.Tests
{
public class JsonSerializationTests
{
public static MyState GetSampleData()
{
return new MyState
{
Id = 11,
Name = "CurrentState",
IsReady = true,
LastUpdated = new DateTime(2015, 10, 21),
Model = new MyModel { FirstName = "Alex",
LastName = "Brown", BirthDate = new DateTime(1990, 1, 12) }
};
}
[Fact]
public void CanDeserializeMyStateTest()
{
var data = GetSampleData();
Assert.Equal(typeof(MyModel), data.Model.GetType());
var json = JsonSerializer.Serialize(data);
var restoredData = JsonSerializer.Deserialize<MyState>(json);
Assert.NotNull(restoredData.Model);
Assert.Equal(typeof(MyModel), restoredData.Model.GetType());
}
}
}
在测试类中,您可以看到 static
方法 GetSampleData
为我们创建了一个测试对象,而在 CanDeserializeMyStateTest
中,我们使用了该方法,尝试将测试对象序列化为 JSON,并将其反序列化到 restoredData
变量。然后,我们检查 restoredData.Model.GetType()
是否为 typeof(MyModel)
,但如果您运行测试,这个 Assert
将失败。JsonSerializer 未识别 Model 类型,而是将其放入了一个包含原始 JSON 数据的 JsonElement
。
让我们帮助 JsonSerializer,在另一个测试中提供 Model 类型来反序列化 JSON 原始数据
[Fact]
public void CanDeserializeMyStateWithJsonElementTest()
{
var data = GetSampleData();
Assert.Equal(typeof(MyModel), data.Model.GetType());
var json = JsonSerializer.Serialize(data);
var restoredData = JsonSerializer.Deserialize<MyState>(json);
Assert.NotNull(restoredData.Model);
Assert.Equal(typeof(JsonElement), restoredData.Model.GetType());
var modelJsonElement = (JsonElement)restoredData.Model;
var modelJson = modelJsonElement.GetRawText();
restoredData.Model = JsonSerializer.Deserialize<MyModel>(modelJson);
Assert.Equal(typeof(MyModel), restoredData.Model.GetType());
}
如果您运行此测试,它将通过,因为现在我们从 restoredData.Model
中读取 JsonElement
并显式地对其进行了反序列化
restoredData.Model = JsonSerializer.Deserialize<MyModel>(modelJson);
因此,当知道 Model
属性对象的类型时,我们可以轻松地从原始 JSON 中恢复它。
现在有了可用的原型,我们可以在 DemoSystemTextJson
项目中将我们的实现封装到一个类中,并将 Model
类型存储在 JSON 的某个位置。
修改 Model 方法
存储 Model
类型最简单直接的方法是扩展 MyState
类并为其添加 ModelFullName
属性。
让我们在 DemoSystemTextJson
项目中创建一个 IJsonModelWrapper
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson
{
public interface IJsonModelWrapper
{
string ModelFullName { get; set; }
}
}
然后,我们将 MyStateModified
类添加到测试项目中,以独立测试此方法
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson.Tests
{
public class MyStateModified : IJsonModelWrapper
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsReady { get; set; }
public DateTime LastUpdated { get; set; }
public object Model { get; set; }
// IJsonModelWrapper
public string ModelFullName { get; set; }
}
}
MyStateModified
包含与 MyState
类相同的属性,并增加了 ModelFullName
,该属性将存储模型类型以供反序列化。
让我们创建 JsonModelConverter
,它将支持 ModelFullName
属性的填充和消耗
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
namespace DemoSystemTextJson
{
public class JsonModelConverter
{
private readonly Dictionary<string, Type> _modelTypes;
public JsonModelConverter()
{
_modelTypes = new Dictionary<string, Type>();
}
public string Serialize(IJsonModelWrapper source, Type modelType)
{
_modelTypes[modelType.FullName] = modelType;
source.ModelFullName = modelType.FullName;
var json = JsonSerializer.Serialize(source, source.GetType());
return json;
}
public T Deserialize<T>(string json)
where T : class, IJsonModelWrapper, new()
{
var result = JsonSerializer.Deserialize(json, typeof(T)) as T;
var modelName = result.ModelFullName;
var objectProperties = typeof(T).GetProperties(BindingFlags.Public |
BindingFlags.Instance).Where(p => p.PropertyType == typeof(object));
foreach (var property in objectProperties)
{
var model = property.GetValue(result);
if (model is JsonElement)
{
var modelJsonElement = (JsonElement)model;
var modelJson = modelJsonElement.GetRawText();
var restoredModel = JsonSerializer.Deserialize
(modelJson, _modelTypes[modelName]);
property.SetValue(result, restoredModel);
}
}
return result as T;
}
}
}
您可以看到 Serialize
方法根据 Model
类型名称填充 ModelFullName
属性,并且它还在 _modelTypes
字典中保留类型以供反序列化。
Deserialize
方法是泛型的,它期望结果对象类型作为模板参数。
它从反序列化后的对象中读取 ModelFullName
,然后找到所有类型为 object 的属性,并使用 _modelTypes
字典中找到的显式类型对其进行反序列化。
让我们用一个我们添加到测试项目中的单元测试来测试它
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Xunit;
using Xunit.Abstractions;
namespace DemoSystemTextJson.Tests
{
public class JsonModelConverterTests
{
private MyStateModified GetSampleData()
{
return new MyStateModified
{
Id = 11,
Name = "CurrentState",
IsReady = true,
LastUpdated = new DateTime(2015, 10, 21),
Model = new MyModel { FirstName = "Alex",
LastName = "Brown", BirthDate = new DateTime(1990, 1, 12) }
};
}
private readonly ITestOutputHelper _output;
public JsonModelConverterTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void JsonModelConverterSerializeTest()
{
var data = GetSampleData();
var converter = new JsonModelConverter();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize<MyStateModified>(json);
Assert.NotNull(restored.Model);
Assert.True(restored.Model.GetType() == typeof(MyModel));
}
[Fact]
public void JsonModelConverterPerformanceTest()
{
var sw = new Stopwatch();
sw.Start();
var converter = new JsonModelConverter();
for (int i = 0; i < 1000000; i++)
{
var data = GetSampleData();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize<MyStateModified>(json);
}
sw.Stop();
_output.WriteLine
($"JsonModelConverterPerformanceTest elapsed {sw.ElapsedMilliseconds} ms");
}
}
}
如果您运行 JsonModelConverterSerializeTest
,您会看到恢复的对象具有正确的 Model
类型和值。
我还添加了另一个测试 JsonModelConverterPerformanceTest
,它执行一百万次序列化和反序列化操作,并输出该操作的经过时间。
在我机器上大约需要 7 秒。
它有效且速度快,但让我们尝试另一种不需要扩展 model
类的方法。
包装器方法
wrapper
是一个基于 MyState
的独立类,它具有 ModelFullName
属性,让我们在单元测试项目中创建它
using System;
using System.Collections.Generic;
using System.Text;
namespace DemoSystemTextJson.Tests
{
public class MyStateWrapper : MyState, IJsonModelWrapper
{
public string ModelFullName { get; set; }
}
}
JsonWrapperConverter
具有更复杂的实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
namespace DemoSystemTextJson
{
public class JsonWrapperConverter
{
private readonly Dictionary<Type, Type> _wrapperByTypeDictionary;
private readonly Dictionary<string, Type> _modelTypes;
public JsonWrapperConverter()
{
_wrapperByTypeDictionary = new Dictionary<Type, Type>();
_modelTypes = new Dictionary<string, Type>();
}
public void AddModel<M>()
where M : class, new()
{
_modelTypes[typeof(M).FullName] = typeof(M);
}
public void AddWrapper<W, T>()
where W : class, IJsonModelWrapper, new()
where T : class, new()
{
_wrapperByTypeDictionary[typeof(T)] = typeof(W);
}
public IJsonModelWrapper CreateInstance
(object source, Type wrapperType, Type modelType)
{
var json = JsonSerializer.Serialize(source);
var wrapper = JsonSerializer.Deserialize(json, wrapperType) as IJsonModelWrapper;
wrapper.ModelFullName = modelType.FullName;
return wrapper;
}
public string Serialize(object source, Type modelType)
{
Type wrapperType = _wrapperByTypeDictionary[source.GetType()];
var wrapper = CreateInstance(source, wrapperType, modelType);
var json = JsonSerializer.Serialize(wrapper, wrapperType);
return json;
}
public T Deserialize<T>(string json)
where T : class, new()
{
Type wrapperType = _wrapperByTypeDictionary[typeof(T)];
var result = JsonSerializer.Deserialize(json, wrapperType) as IJsonModelWrapper;
var modelName = result.ModelFullName;
var objectProperties = typeof(T).GetProperties(BindingFlags.Public |
BindingFlags.Instance).Where(p => p.PropertyType == typeof(object));
foreach (var property in objectProperties)
{
var model = property.GetValue(result);
if (model is JsonElement)
{
var modelJsonElement = (JsonElement)model;
var modelJson = modelJsonElement.GetRawText();
var restoredModel = JsonSerializer.Deserialize
(modelJson, _modelTypes[modelName]);
property.SetValue(result, restoredModel);
}
}
return result as T;
}
}
}
对于每个源对象类型,我们需要创建一个包装器,并将它们存储在 _modelTypes
和 _wrapperByTypeDictionary
字典中。
AddModel
和 AddWrapper
用于提供源类型和包装器类型并进行存储。
CreateInstance
方法由 Serialize
使用,从源对象创建包装器对象。包装器对象将具有所有源属性和一个额外的属性 – ModelFullName
。
Deserialize
方法再次是泛型的。它在字典中按源类型查找包装器类型,然后反序列化包装器并读取 ModelFullName
。然后它使用反射读取所有动态属性(typeof(object)
)并从原始 JSON 中恢复它们。
为了测试这一点,我们创建了 JsonWrapperConverterTests
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Xunit;
using Xunit.Abstractions;
namespace DemoSystemTextJson.Tests
{
public class JsonWrapperConverterTests
{
private MyState GetSampleData()
{
return new MyState
{
Id = 11,
Name = "CurrentState",
IsReady = true,
LastUpdated = new DateTime(2015, 10, 21),
Model = new MyModel { FirstName = "Alex",
LastName = "Brown", BirthDate = new DateTime(1990, 1, 12) }
};
}
private readonly ITestOutputHelper _output;
public JsonWrapperConverterTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public void JsonWrapperConverterSerializeTest()
{
var data = GetSampleData();
var converter = new JsonWrapperConverter();
converter.AddWrapper<MyStateWrapper, MyState>();
converter.AddModel<MyModel>();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize<MyState>(json);
Assert.NotNull(restored.Model);
Assert.True(restored.Model.GetType() == typeof(MyModel));
}
[Fact]
public void JsonWrapperConverterPerformanceTest()
{
var sw = new Stopwatch();
sw.Start();
var converter = new JsonWrapperConverter();
converter.AddWrapper<MyStateWrapper, MyState>();
converter.AddModel<MyModel>();
for (int i = 0; i < 1000000; i++)
{
var data = GetSampleData();
var json = converter.Serialize(data, data.Model.GetType());
var restored = converter.Deserialize<MyState>(json);
}
sw.Stop();
_output.WriteLine($"JsonWrapperConverterPerformanceTest elapsed
{sw.ElapsedMilliseconds} ms");
}
[Fact]
public void JsonNewtonsoftPerformanceTest()
{
var sw = new Stopwatch();
sw.Start();
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameAssemblyFormatHandling =
Newtonsoft.Json.TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
};
for (int i = 0; i < 1000000; i++)
{
var data = GetSampleData();
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data, settings);
var restored = Newtonsoft.Json.JsonConvert.DeserializeObject<MyState>(json);
}
sw.Stop();
_output.WriteLine($"JsonNewtonsoftPerformanceTest elapsed
{sw.ElapsedMilliseconds} ms");
}
}
}
如果您运行 JsonWrapperConverterSerializeTest
,您会看到包装器方法也有效。
我还添加了 JsonWrapperConverterPerformanceTest
和 JsonNewtonsoftPerformanceTest
来检查它们的性能。
如果您运行所有性能测试,您将能够看到与我的相似的结果
JsonModelConverterPerformanceTest | 5654 毫秒 |
JsonWrapperConverterSerializeTest | 9760 毫秒 |
JsonNewtonsoftPerformanceTest | 10671 毫秒 |
摘要
今天,我们已经表明,如果您需要将项目从 Newtonsoft 迁移到 System.Text.Json
序列化器,您将遇到一些困难,因为 System.Text.Json
序列化器不支持使用 “$type”
属性进行动态对象反序列化。我们实现了两种方法来序列化和反序列化动态对象,通过注入 ModelFullName
属性来实现按 Model
类型进行显式反序列化。
如果您可以修改您的 model
类并添加 ModelFullName
属性,您可以使用最快最简单的序列化,但如果您不能更改您的 model
类,您可以使用一种仍然比 Newtonsoft 序列化更快的方法,即包装器方法。
历史
- 2020 年 11 月 6 日:初始版本