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

ASP.NET Core 8 Web API数据验证为何忽略Data Annotations自定义错误信息?

解决ApiController中DataAnnotations自定义错误信息不生效的问题

你遇到的核心问题是C# required关键字与DataAnnotations验证的执行时机冲突:

  • required是JSON反序列化阶段的检查,由System.Text.Json在创建对象时触发,一旦发现缺少标记为required的属性,直接抛出系统默认错误。
  • [Required]是Model验证阶段的检查,只有当对象成功完成反序列化后,ASP.NET Core才会执行DataAnnotations验证逻辑。

当你发送空对象{}时,反序列化器先报错,根本没走到Model验证环节,自然不会返回你配置的自定义错误信息。

下面提供三种可行的解决办法:

方案1:移除required关键字,仅保留[Required]特性

修改UpdateAboutPayload,去掉属性的required修饰符,允许属性为null,让反序列化能成功创建对象,之后Model验证会触发[Required]的自定义错误:

using System.ComponentModel.DataAnnotations;

namespace ClassLibrary.Models.API.Requests
{
    public class UpdateAboutPayload
    {
        [Required(ErrorMessage = "About is required")]
        [StringLength(500, ErrorMessage = "About must be between 0 and 500 characters long")]
        public string? About { get; set; }
    }
}

修改后发送{},会返回你配置的自定义错误:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "About": [
      "About is required"
    ]
  },
  "traceId": "..."
}

方案2:配置System.Text.Json禁用required属性的强制检查(.NET 7+)

如果你想保留required关键字,可以在Program.cs中配置Json选项,让反序列化器不强制要求required属性,从而让Model验证接管检查逻辑:

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        // 核心配置:禁用required属性的反序列化强制检查
        options.JsonSerializerOptions.RequiredPropertySelector = static (property) => false;
        // 可选:保持属性名大小写不敏感
        options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
    });

方案3:自定义ProblemDetailsFactory替换错误信息

如果必须保留required关键字的反序列化检查,同时想自定义错误信息,可以实现自定义的ProblemDetailsFactory,替换默认的错误内容:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Diagnostics;

public class CustomProblemDetailsFactory : ProblemDetailsFactory
{
    private readonly ApiBehaviorOptions _options;

    public CustomProblemDetailsFactory(IOptions<ApiBehaviorOptions> options)
    {
        _options = options.Value ?? throw new ArgumentNullException(nameof(options));
    }

    public override ProblemDetails CreateProblemDetails(
        HttpContext httpContext,
        int? statusCode = null,
        string? title = null,
        string? type = null,
        string? detail = null,
        string? instance = null)
    {
        statusCode ??= 500;

        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Type = type,
            Detail = detail,
            Instance = instance,
        };

        ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);

        return problemDetails;
    }

    public override ValidationProblemDetails CreateValidationProblemDetails(
        HttpContext httpContext,
        ModelStateDictionary modelStateDictionary,
        int? statusCode = null,
        string? title = null,
        string? type = null,
        string? detail = null,
        string? instance = null)
    {
        if (modelStateDictionary == null)
        {
            throw new ArgumentNullException(nameof(modelStateDictionary));
        }

        statusCode ??= 400;

        var problemDetails = new ValidationProblemDetails(modelStateDictionary)
        {
            Status = statusCode,
            Type = type,
            Detail = detail,
            Instance = instance,
        };

        if (title != null)
        {
            problemDetails.Title = title;
        }

        ApplyProblemDetailsDefaults(httpContext, problemDetails, statusCode.Value);

        // 替换反序列化阶段的默认错误信息
        if (problemDetails.Errors.TryGetValue("$", out var errors))
        {
            problemDetails.Errors.Remove("$");
            problemDetails.Errors.Add("About", new[] { "About is required" });
        }

        // 移除多余的payload错误提示
        problemDetails.Errors.Remove("payload");

        return problemDetails;
    }

    private void ApplyProblemDetailsDefaults(HttpContext httpContext, ProblemDetails problemDetails, int statusCode)
    {
        problemDetails.Status ??= statusCode;

        if (_options.ClientErrorMapping.TryGetValue(statusCode, out var clientErrorData))
        {
            problemDetails.Title ??= clientErrorData.Title;
            problemDetails.Type ??= clientErrorData.Link;
        }

        var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
        if (traceId != null)
        {
            problemDetails.Extensions["traceId"] = traceId;
        }
    }
}

然后在Program.cs中注册这个自定义工厂:

builder.Services.AddSingleton<ProblemDetailsFactory, CustomProblemDetailsFactory>();

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 02:55:56