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."); } }
关键改进
- 保留默认序列化配置:使用
serializer.Deserialize/serializer.Populate替代jToken.ToObject,确保沿用Web API的MissingMemberHandling等默认规则,不会丢失未匹配属性的处理行为。 - 递归处理嵌套结构:覆盖对象中所有层级的数组属性,包括嵌套数组。
- 安全移除空项:反向遍历数组避免删除元素时的索引错乱问题。
- 保持原逻辑一致性:继续将空数组转为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
相关产品推荐
相关产品推荐

