直接用JsonConvert.DeserializeObject不可靠,因默认忽略private set、readonly属性,无法处理循环引用及特殊类型;应改用反射遍历public可读属性,递归处理嵌套对象并限制深度,缓存PropertyInfo提升性能。

为什么直接用 JsonConvert.DeserializeObject<dictionary object>></dictionary> 不可靠
很多开发者一上来就想把对象序列化成 JSON 再反序列化为 Dictionary<string object></string>,这在属性全是 public、无循环引用、无特殊类型(如 DateTimeOffset、DBNull、自定义类嵌套)时看似能跑通,但实际极易崩:Newtonsoft.Json 默认不处理 null 值的键名映射,遇到 private set 属性会丢数据,对 readonly 字段或 get-only auto-property(C# 6+)直接跳过。更麻烦的是,一旦对象里有 IDictionary 或 IEnumerable 成员,反序列化结果可能是嵌套字典或数组,而非你期望的扁平键值对。
用反射遍历 GetProperty + GetValue 是最可控的起点
核心思路是:只读取 public 实例属性(排除索引器、静态属性、方法),逐个调用 GetValue 获取值,再按需做类型适配。关键不是“全量转”,而是“可控转”:
-
PropertyInfo.CanRead必须为true,且PropertyInfo.GetMethod非null且非静态 - 跳过属性名为
"Item"(索引器)、"Count"、"Keys"、"Values"等集合元信息属性 - 对
DateTime、Guid、Enum类型建议统一转成字符串(避免下游解析歧义),可用Convert.ToString(value) - 对
null值保留(不跳过),由调用方决定是否过滤 —— 很多 API 请求体需要显式传null表示字段清空
示例片段:
var dict = new Dictionary<string object>();
var props = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
if (!prop.CanRead || prop.GetMethod == null || prop.GetMethod.IsStatic) continue;
if (new[] { "Item", "Count", "Keys", "Values" }.Contains(prop.Name)) continue;
<pre class="brush:php;toolbar:false;">var value = prop.GetValue(obj);
// 类型规整:DateTime → string, Enum → name, null → null
if (value is DateTime dt) value = dt.ToString("o");
else if (value is Enum e) value = e.ToString();
else if (value is IConvertible) value = Convert.ToString(value);
dict[prop.Name] = value;
}
嵌套对象怎么处理?递归还是扁平化得看用途
如果目标是生成 HTTP 请求 body(比如调用 REST API),通常需要扁平化(user.profile.name),而不是嵌套字典;如果是做配置映射或日志结构化,嵌套更自然。反射本身不解决层级问题,得加逻辑分支:
- 若属性类型是 class / struct(且非 string / primitive / DateTime / Guid / Enum),默认递归调用同一转换方法,生成嵌套
Dictionary<string object></string> - 若需扁平化,改用
Stack迭代 + 拼接键名,避免栈溢出(比纯递归安全) - 必须限制递归深度(比如最大 5 层),否则遇到循环引用(A→B→A)直接
StackOverflowException - 对
IEnumerable但非string的类型(如List<int></int>),转成Array或List<object></object>,不继续展开元素(除非明确要求深度展开)
性能和兼容性要注意这些硬伤
反射慢是事实,但首次获取 PropertyInfo[] 后缓存起来(比如用 ConcurrentDictionary<type propertyinfo></type>),后续同类型对象转换可提速 5–10 倍。真正容易被忽略的是 .NET 版本差异:
- .NET 5+ 支持
Type.GetProperties()返回顺序稳定(按声明顺序),但 .NET Framework 4.8 及之前不保证 —— 如果依赖键序(比如签名计算),得手动按Name排序 -
Nullable<t></t>属性(如int?)的GetValue返回null或底层值,无需额外解包;但typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable)这种判断在 AOT 编译(.NET 7+)下可能失效,建议用Nullable.GetUnderlyingType - 如果对象来自 COM 或动态类型(
ExpandoObject),GetProperties返回空数组 —— 此时得 fallback 到IDictionary接口尝试转换
通用性不在于覆盖所有边缘类型,而在于明确边界:只承诺支持 public 属性、可读、非索引器、非静态,并在文档里写清不支持 dynamic、COM 对象、指针类型。其余情况抛 NotSupportedException,比静默丢数据更利于排错。











