JSON序列化中同名类属性的精准序列化控制问题
实现层级结构下的精准属性序列化过滤
当然可以实现这个需求!问题的核心在于不能只看属性名,而是要追踪属性在对象层级中的完整路径。下面我会给你两种实用的解决方案,都是基于Json.NET(Newtonsoft.Json)来实现的,你可以根据自己的场景选择。
方案一:基于自定义ContractResolver的路径过滤
这种方案适合需要和其他ContractResolver特性(比如驼峰命名、忽略空值)结合的场景,核心思路是给每个属性标记完整路径,然后通过路径判断是否序列化。
代码实现
public class PathBasedContractResolver : DefaultContractResolver { private readonly HashSet<string> _allowedPaths; public PathBasedContractResolver(HashSet<string> allowedPaths) { _allowedPaths = allowedPaths; // 禁用契约缓存:因为同一个类型在不同层级下的路径不同,缓存会导致判断逻辑混乱 CacheContract = false; } protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) { return CreatePropertiesWithPath(type, string.Empty); } private IList<JsonProperty> CreatePropertiesWithPath(Type type, string parentPath) { var properties = new List<JsonProperty>(); var serializableMembers = GetSerializableMembers(type); foreach (var member in serializableMembers) { var prop = CreateProperty(member, MemberSerialization.OptOut); // 生成当前属性的完整路径 var fullPath = string.IsNullOrEmpty(parentPath) ? prop.PropertyName : $"{parentPath}.{prop.PropertyName}"; // 核心判断逻辑: // 1. 如果当前路径在允许列表里,直接序列化 // 2. 如果当前路径的子路径在允许列表里,也要序列化(否则子属性无法被访问到) prop.ShouldSerialize = instance => { if (_allowedPaths.Contains(fullPath)) return true; return _allowedPaths.Any(p => p.StartsWith($"{fullPath}.", StringComparison.Ordinal)); }; properties.Add(prop); // 递归处理嵌套的复杂类型(排除值类型和字符串) if (prop.PropertyType.IsClass && prop.PropertyType != typeof(string) && !prop.PropertyType.IsValueType) { CreatePropertiesWithPath(prop.PropertyType, fullPath); } } return properties; } }
使用示例
// 定义需要保留的属性路径(相对于根对象Product) var allowedPaths = new HashSet<string> { "ProductPrice.Amount.Value.valueId" }; var resolver = new PathBasedContractResolver(allowedPaths); var settings = new JsonSerializerSettings { ContractResolver = resolver, Formatting = Formatting.Indented // 格式化输出方便查看 }; // 测试对象 var product = new Product { ProductPrice = new ProductPrice { Amount = new Amount { Value = new Value { valueId = "PRICE_123" } } }, ProductDiscount = new ProductDiscount { Amount = new Amount { Value = new Value { valueId = "DISCOUNT_456" } } } }; // 序列化 var json = JsonConvert.SerializeObject(product, settings);
序列化后只会保留ProductPrice.Amount.Value.valueId这条路径,ProductDiscount整个分支会被完全忽略。
方案二:基于自定义JsonConverter的动态路径追踪
如果不需要结合其他ContractResolver特性,这种方案更简洁,性能也更好——它会在序列化过程中实时追踪当前属性的路径,然后决定是否序列化。
代码实现
public class PathTrackingConverter : JsonConverter { private readonly HashSet<string> _allowedPaths; private readonly Stack<string> _pathStack = new Stack<string>(); public PathTrackingConverter(HashSet<string> allowedPaths) { _allowedPaths = allowedPaths; } public override bool CanConvert(Type objectType) { // 只处理自定义类(排除基本类型和字符串) return objectType.IsClass && objectType != typeof(string); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { writer.WriteStartObject(); var type = value.GetType(); // 遍历当前对象的所有可读写公共属性 var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead); foreach (var prop in properties) { var propName = serializer.ContractResolver.ResolveName(prop); // 生成当前属性的完整路径 var currentPath = _pathStack.Any() ? string.Join(".", _pathStack.Reverse()) + "." + propName : propName; // 判断是否允许序列化当前属性(包括子路径是否被允许) var isAllowed = _allowedPaths.Contains(currentPath) || _allowedPaths.Any(p => p.StartsWith(currentPath + ".", StringComparison.Ordinal)); if (!isAllowed) continue; var propValue = prop.GetValue(value); writer.WritePropertyName(propName); if (propValue == null) { writer.WriteNull(); continue; } // 递归处理嵌套对象,更新路径栈 var propType = propValue.GetType(); if (propType.IsClass && propType != typeof(string)) { _pathStack.Push(propName); serializer.Serialize(writer, propValue); _pathStack.Pop(); } else { serializer.Serialize(writer, propValue); } } writer.WriteEndObject(); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new NotImplementedException("此转换器仅用于序列化"); } }
使用示例
var allowedPaths = new HashSet<string> { "ProductPrice.Amount.Value.valueId" }; var settings = new JsonSerializerSettings { Converters = new List<JsonConverter> { new PathTrackingConverter(allowedPaths) }, Formatting = Formatting.Indented }; var json = JsonConvert.SerializeObject(product, settings);
这个方案的效果和第一种完全一样,但不需要修改ContractResolver的缓存逻辑,更适合简单的路径过滤场景。
关键注意点
- 路径的定义:路径是相对于根对象的,比如根是
Product,那么Product.ProductPrice.Amount.Value.valueId对应的路径就是ProductPrice.Amount.Value.valueId,不需要包含根对象本身。 - 父属性的序列化:如果要保留子属性,它的所有父属性都会被自动序列化(比如要保留
valueId,ProductPrice、Amount、Value都会出现在JSON里),这是JSON结构的要求,否则子属性无法存在。 - 性能考量:第一种方案禁用了契约缓存,如果序列化大量对象,性能会有所下降;第二种方案没有这个问题,性能更优。
内容的提问来源于stack exchange,提问作者Artyom Valeev
相关产品推荐
相关产品推荐

