JsonConvert.SerializeObject子线程调用GetProperty后字段排序异常如何解决
问题复现
JsonConvert.SerializeObject 会在子线程中对被序列化对象调用 .GetProperty 方法时改变JSON的字段排序顺序。
复现代码如下:
class Program { static void Main(string[] args) { var tasks = new List<Task>(); for (int i = 0; i < 10; i++) { var task = Task.Factory.StartNew(() => { var token = CreateRandomToken(); _ = typeof(TestObject).GetProperty("Version"); var str = JsonConvert.SerializeObject(token); Console.WriteLine(str); }); tasks.Add(task); } Task.WaitAll(tasks.ToArray()); Console.ReadLine(); } private static TestObject CreateRandomToken() => new TestObject { TokenHash = "123456789", Name = "Name", Version = "123" }; } public class TestObject { public string TokenHash { get; set; } public string Name { get; set; } public string Version { get; set; } }
现象说明
- 执行代码后
Version字段会出现在JSON的最前位置,而非类定义时的末尾位置 - 移除
_ = typeof(TestObject).GetProperty("Version");代码后,字段排序不会发生变化;在主线程中执行该段代码,字段排序也不会发生变化 - 即使为对象属性添加
[JsonProperty (Order = 1)]特性,最终排序也无法符合特性指定的顺序
补充说明:业务中使用JSON字符串生成数字签名,字段顺序变化会导致数字签名失效,因此字段顺序必须固定。
根因说明
该问题是.NET反射缓存机制和Newtonsoft.Json默认排序逻辑共同作用的结果:
- .NET的
Type.GetProperty方法内部会缓存反射结果,多线程环境下单独获取某一个属性时,该属性会被优先加入缓存,后续调用GetProperties返回的属性列表顺序会和缓存填充顺序一致 - Newtonsoft.Json默认没有强制排序规则,序列化时直接使用反射返回的属性列表顺序,因此会被外部反射调用打乱
解决方案(无需使用
[JsonProperty(Order)]特性) 方案1:自定义序列化契约固定顺序(最稳妥)
自定义ContractResolver强制序列化时按照类定义的属性顺序排序,不受外部反射调用影响:
public class OrderedPropertyContractResolver : DefaultContractResolver { protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) { // 按属性的MetadataToken排序,对应代码中定义的先后顺序 return base.CreateProperties(type, memberSerialization) .OrderBy(p => type.GetProperty(p.PropertyName)?.MetadataToken ?? int.MaxValue) .ToList(); } }
使用方式有两种:
- 全局配置,所有序列化默认生效:
// 程序启动时执行一次即可 JsonConvert.DefaultSettings = () => new JsonSerializerSettings { ContractResolver = new OrderedPropertyContractResolver() };
- 单次序列化指定,仅当前调用生效:
var jsonStr = JsonConvert.SerializeObject(token, new JsonSerializerSettings { ContractResolver = new OrderedPropertyContractResolver() });
方案2:预热反射缓存(改造成本最低)
如果需要固定顺序的类型数量较少,可以在程序启动的主线程中提前调用一次GetProperties,提前按定义顺序填充反射缓存,后续多线程调用不会打乱顺序:
// 程序启动时执行一次即可 _ = typeof(TestObject).GetProperties();
方案3:迁移到System.Text.Json
.NET自带的System.Text.Json序列化库默认就是按属性声明顺序序列化,不存在该多线程反射导致顺序混乱的问题,如果业务允许迁移序列化库,也是长期稳定的解决方案。
内容的提问来源于stack exchange,提问作者Max
相关产品推荐
相关产品推荐

