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

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的缓存逻辑,更适合简单的路径过滤场景。


关键注意点

  1. 路径的定义:路径是相对于根对象的,比如根是Product,那么Product.ProductPrice.Amount.Value.valueId对应的路径就是ProductPrice.Amount.Value.valueId,不需要包含根对象本身。
  2. 父属性的序列化:如果要保留子属性,它的所有父属性都会被自动序列化(比如要保留valueId,ProductPrice、Amount、Value都会出现在JSON里),这是JSON结构的要求,否则子属性无法存在。
  3. 性能考量:第一种方案禁用了契约缓存,如果序列化大量对象,性能会有所下降;第二种方案没有这个问题,性能更优。

内容的提问来源于stack exchange,提问作者Artyom Valeev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:46:50