ASP.NET Core 3.1中验证请求体非可空必填属性的问题
我完全理解你的困惑——.NET Core 3.0+的验证逻辑确实有点反直觉:它把非可空参数/属性视为隐含[Required],但实际验证时,因为值类型永远不会为null,就算请求里完全省略这个字段,反序列化后会被赋值为类型默认值(比如int是0),导致[Required]特性根本不会触发错误。
下面提供几种可行的解决方案,既能保持模型类型的准确性(不用改成可空类型),又能在字段未提供时让ModelState标记为无效:
方案1:使用JSON序列化特有的必填特性
这个方案针对JSON请求体,分别适配System.Text.Json和Newtonsoft.Json两种序列化器:
对于System.Text.Json(ASP.NET Core 3.0+默认)
在模型属性上添加[JsonRequired]特性(来自System.Text.Json.Serialization命名空间):
using System.Text.Json.Serialization; public class NonNullablesRequest { [JsonRequired] public int NonNullableInt { get; set; } }
当请求体中完全省略NonNullableInt字段时,JSON反序列化会直接抛出JsonException,ASP.NET Core会自动将这个异常转化为ModelState错误,返回400 BadRequest,同时会包含类似"Required property 'NonNullableInt' not found in JSON."的错误信息。
对于Newtonsoft.Json(如果项目配置了使用它)
使用[JsonProperty(Required = Required.Always)]特性:
using Newtonsoft.Json; public class NonNullablesRequest { [JsonProperty(Required = Required.Always)] public int NonNullableInt { get; set; } }
效果和上面一致,反序列化时缺少字段会抛出异常,最终转化为ModelState错误。
方案2:自定义验证属性
如果你希望不依赖序列化器特性,或者需要更灵活的验证逻辑,可以自定义一个验证属性,结合模型绑定的上下文来判断字段是否被提交:
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using System.ComponentModel.DataAnnotations; public class RequiredWhenMissingAttribute : ValidationAttribute, IClientModelValidator { public override bool IsValid(object value) { // 基础判断:排除默认值 return value != null && !value.Equals(default); } public override void Validate(ValidationContext validationContext) { base.Validate(validationContext); // 获取模型绑定元数据,判断属性是否在请求中被提交 var modelMetadata = validationContext.Items["ModelMetadata"] as ModelMetadata; if (modelMetadata != null && !modelMetadata.IsBound) { validationContext.AddValidationError(ErrorMessage ?? $"The field {modelMetadata.DisplayName} is required."); } } public void AddValidation(ClientModelValidationContext context) { // 可选:添加客户端验证规则 context.Attributes.Add("data-val", "true"); context.Attributes.Add("data-val-requiredwhenmissing", ErrorMessage ?? $"The field {context.ModelMetadata.DisplayName} is required."); } }
然后在模型中使用:
public class NonNullablesRequest { [RequiredWhenMissing(ErrorMessage = "NonNullableInt is required.")] public int NonNullableInt { get; set; } }
这个属性会同时检查两个条件:属性值是否为类型默认值,以及该属性是否在请求中被绑定(即是否存在于请求体),如果两者都满足,就标记为验证错误。
方案3:全局配置JSON序列化行为(谨慎使用)
如果你希望所有非可空值类型在未提供时都触发验证错误,可以全局配置JSON序列化器的必填规则:
System.Text.Json全局配置
在Program.cs中修改:
builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyRequired = JsonRequired.Always; });
这个配置会让所有非可空属性都被视为必填,任何未提供的非可空字段都会导致反序列化失败。但要注意,这是全局设置,会影响所有模型,可能不符合某些场景的需求。
补充说明
为什么默认情况下[Required]对非可空值类型无效?
.NET Core 3.0+的验证系统确实会将非可空参数/属性视为隐含
[Required],但[Required]特性的核心逻辑是检查值是否为null。对于值类型(比如int、decimal),它们永远不可能为null——就算请求中省略字段,反序列化也会赋值为类型默认值,所以[Required]永远不会触发错误。这就是你遇到的矛盾点:框架认为非可空类型"必填",但验证逻辑又因为值不为null而判定有效。
而上面的解决方案,要么通过序列化器强制检查字段存在性,要么通过自定义验证补充了"字段是否被提交"的判断,完美解决了这个问题,同时保持了模型类型的准确性,不用改成可空类型。
内容的提问来源于stack exchange,提问作者Oliver

