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

ASP.NET Core 3 Web API:模型验证前移除JSON数组空项

问题描述

我需要在ASP.NET Core 3 Web API的模型验证前,移除用户提交的JSON负载中数组属性的空项。具体场景如下:

  • 模型已添加Required等验证特性
  • API方法有两种绑定形式:[FromBody]List<MyModelEntity> 和 [FromBody]MyModelEntity
  • GET请求无需处理空项

我最初通过自定义JsonConverter实现功能,但存在反射性能问题:

public class NullListFilterConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsGenericType &&
            (objectType.GetGenericTypeDefinition() == typeof(List<>) || objectType.GetGenericTypeDefinition() == typeof(IList<>)))
        {
            Type itemType = objectType.GetGenericArguments()[0];
            return itemType.IsClass;
        }
        return false;
    }

    // 存在反射导致的性能问题
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        var list = existingValue as IList
            ?? (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(objectType.GetGenericArguments()));

        serializer.Populate(reader, list);

        // 过滤列表中的空项
        var nonNullItems = list.Cast<object>().Where(x => x != null).ToList();
        list.Clear();
        foreach (var item in nonNullItems)
        {
            list.Add(item);
        }

        // 空列表返回null
        return list.Cast<object>().Count() == 0 ? null : list;
    }

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");
    }
}

这个实现能完成两个需求:1. 移除JSON数组中的空项;2. 处理未匹配属性时保持实体默认置空行为,但反射会影响性能。

后来尝试无反射实现,但丢失了未匹配属性置空的默认行为:

// 无反射实现,但丢失未匹配属性置空行为
public object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var jToken = JToken.Load(reader);
    var nullTokens = jToken.SelectTokens("$..[?(@ == null)]");
    nullTokens?.ToList().ForEach(token =>
    {
        if (token.Parent is JArray parentArray)
        {
            parentArray.Remove(token);
        }
        else if (token.Parent is JProperty parentProperty)
        {
            parentProperty.Value = JValue.CreateNull();
        }
    });

    return jToken.ToObject(objectType);
}

请问如何解决无反射实现丢失默认行为的问题?有没有其他移除JSON数组空项的方法?


解决方案

修复无反射JsonConverter的问题

问题核心是jToken.ToObject(objectType)没有沿用Web API默认的序列化配置,导致丢失了未匹配属性的处理逻辑。修改后的实现如下:

public class NullListFilterConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        if (objectType.IsGenericType &&
            (objectType.GetGenericTypeDefinition() == typeof(List<>) || objectType.GetGenericTypeDefinition() == typeof(IList<>)))
        {
            Type itemType = objectType.GetGenericArguments()[0];
            return itemType.IsClass;
        }
        return false;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jToken = JToken.Load(reader);
        
        // 递归移除所有数组中的空项
        RemoveNullArrayItems(jToken);

        // 使用原序列化器反序列化,保留Web API默认配置
        return existingValue != null 
            ? serializer.Populate(jToken.CreateReader(), existingValue) 
            : serializer.Deserialize(jToken.CreateReader(), objectType);
    }

    private void RemoveNullArrayItems(JToken token)
    {
        if (token.Type == JTokenType.Array)
        {
            var array = (JArray)token;
            // 反向遍历避免索引偏移
            for (int i = array.Count - 1; i >= 0; i--)
            {
                if (array[i].Type == JTokenType.Null)
                {
                    array.RemoveAt(i);
                }
                else
                {
                    // 递归处理嵌套数组
                    RemoveNullArrayItems(array[i]);
                }
            }
            // 空数组转为null,保持原逻辑
            if (array.Count == 0 && token.Parent is JProperty property)
            {
                property.Value = JValue.CreateNull();
            }
        }
        else if (token.Type == JTokenType.Object)
        {
            // 递归处理对象中的所有属性
            foreach (var child in token.Children<JProperty>())
            {
                RemoveNullArrayItems(child.Value);
            }
        }
    }

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");
    }
}

关键改进

  1. 保留默认序列化配置:使用serializer.Deserialize/serializer.Populate替代jToken.ToObject,确保沿用Web API的MissingMemberHandling等默认规则,不会丢失未匹配属性的处理行为。
  2. 递归处理嵌套结构:覆盖对象中所有层级的数组属性,包括嵌套数组。
  3. 安全移除空项:反向遍历数组避免删除元素时的索引错乱问题。
  4. 保持原逻辑一致性:继续将空数组转为null值,与最初的反射实现行为对齐。

其他可选方案

方案1:自定义ContractResolver

通过修改Json.NET的契约解析器,在反序列化后自动过滤空项:

public class NullItemFilterContractResolver : DefaultContractResolver
{
    protected override JsonArrayContract CreateArrayContract(Type objectType)
    {
        var contract = base.CreateArrayContract(objectType);
        
        // 添加反序列化后回调,过滤空项
        contract.OnDeserializedCallbacks.Add((obj, streamingContext) =>
        {
            if (obj is IList list)
            {
                var nonNullItems = list.Cast<object>().Where(x => x != null).ToList();
                list.Clear();
                foreach (var item in nonNullItems)
                {
                    list.Add(item);
                }
            }
        });
        return contract;
    }
}

在Startup中配置:

services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = new NullItemFilterContractResolver();
    });

方案2:使用ActionFilterAttribute

在模型绑定完成后、验证前执行空项过滤:

public class RemoveNullListItemsFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        foreach (var argument in context.ActionArguments.Values)
        {
            if (argument == null) continue;
            
            // 处理直接传入的List<T>
            if (argument is IList list)
            {
                FilterNullItems(list);
            }
            else
            {
                // 处理实体对象中的数组/列表属性
                var listProperties = argument.GetType().GetProperties()
                    .Where(p => typeof(IList).IsAssignableFrom(p.PropertyType) && p.PropertyType.IsGenericType);
                
                foreach (var prop in listProperties)
                {
                    if (prop.GetValue(argument) is IList propList)
                    {
                        FilterNullItems(propList);
                    }
                }
            }
        }
        base.OnActionExecuting(context);
    }

    private void FilterNullItems(IList list)
    {
        var nonNullItems = list.Cast<object>().Where(x => x != null).ToList();
        list.Clear();
        foreach (var item in nonNullItems)
        {
            list.Add(item);
        }
    }
}

注册全局过滤器:

services.AddControllers(options =>
{
    options.Filters.Add<RemoveNullListItemsFilter>();
});

方案对比

方案优点缺点
自定义JsonConverter反序列化阶段处理,性能最优,逻辑集中需要适配Json.NET的序列化规则
ContractResolver与序列化系统深度集成,无需修改Controller空数组转null的逻辑需要额外调整
ActionFilter实现简单,无序列化侵入处理时机晚于反序列化,若验证依赖空项移除可能出现问题

内容的提问来源于stack exchange,提问作者Anil Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 23:32:02