You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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默认排序逻辑共同作用的结果:

  1. .NET的Type.GetProperty方法内部会缓存反射结果,多线程环境下单独获取某一个属性时,该属性会被优先加入缓存,后续调用GetProperties返回的属性列表顺序会和缓存填充顺序一致
  2. 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();
    }
}

使用方式有两种:

  1. 全局配置,所有序列化默认生效:
// 程序启动时执行一次即可
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
    ContractResolver = new OrderedPropertyContractResolver()
};
  1. 单次序列化指定,仅当前调用生效:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 11:06:03