从Newtonsoft迁移到System.Text.Json时保留DataAnnotations行为的方法
.NET 8 WebAPI迁移System.Text.Json时required关键字验证提示差异问题
问题场景
我正在将一个使用Newtonsoft.Json的.NET 8 WebAPI迁移到System.Text.Json,遇到了required关键字相关的验证提示差异,以下用默认的WeatherForecast示例说明:
Program.cs
// 省略部分代码 builder.Services.AddControllers(); // 省略部分代码
WeatherForecast.cs
public class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); [Required] public required string Summary { get; set; } }
WeatherForecastController.cs
// 省略部分代码 [HttpPost(Name = "WeatherForecast")] public IActionResult Post(WeatherForecast request) { return Ok(request); }
差异表现
当通过Swagger调用Post接口,请求体未传入Summary属性时:
{ "date": "2024-01-01", "temperatureC": 1 }
System.Text.Json的错误返回
{ "errors": { "$": [ "JSON deserialization for type 'NewtonsoftVsSystemTextLab.WeatherForecast' was missing required properties, including the following: summary" ], "request": [ "The request field is required." ] } }
Newtonsoft.Json的错误返回(仅需添加.AddNewtonsoftJson()扩展方法)
{ "errors": { "Summary": [ "The Summary field is required." ] } }
提问
有没有无需修改源代码的非侵入式方案,让System.Text.Json输出和Newtonsoft.Json一致的友好验证提示?
非侵入式解决方案
可以通过配置JsonOptions并添加自定义模型验证过滤器实现,全程不需要改动实体类代码:
1. 关闭System.Text.Json的内置required属性验证
在Program.cs中修改控制器配置,让System.Text.Json不再自动校验required属性,转而交给DataAnnotations的[Required]特性处理:
builder.Services.AddControllers() .AddJsonOptions(options => { // 关闭required属性的反序列化强制校验 options.JsonSerializerOptions.RequiredPropertyHandling = JsonRequiredPropertyHandling.Ignore; // 保留原有Json配置(比如驼峰命名、大小写不敏感) options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; });
2. 编写自定义验证过滤器
创建一个过滤器,捕获System.Text.Json的反序列化错误,将其转换为DataAnnotations风格的字段级错误提示:
using Microsoft.AspNetCore.Mvc.Filters; using System.Text.RegularExpressions; public class RequiredPropertyValidationFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { var errorsToRemove = new List<string>(); var correctedErrors = new Dictionary<string, List<string>>(); foreach (var modelError in context.ModelState) { // 处理根节点的反序列化错误(键为"$"的条目) if (modelError.Key == "$") { foreach (var error in modelError.Value.Errors) { // 从错误信息中提取缺失的属性名 var match = Regex.Match(error.ErrorMessage, @"including the following: (\w+)"); if (match.Success) { var camelCaseProp = match.Groups[1].Value; // 转换为实体类的PascalCase属性名 var pascalCaseProp = char.ToUpper(camelCaseProp[0]) + camelCaseProp.Substring(1); if (!correctedErrors.ContainsKey(pascalCaseProp)) { correctedErrors[pascalCaseProp] = new List<string>(); } correctedErrors[pascalCaseProp].Add($"The {pascalCaseProp} field is required."); } } errorsToRemove.Add(modelError.Key); } // 移除无意义的"request"字段错误 else if (modelError.Key == "request") { errorsToRemove.Add(modelError.Key); } } // 清理旧的错误条目 foreach (var key in errorsToRemove) { context.ModelState.Remove(key); } // 添加转换后的字段级错误 foreach (var entry in correctedErrors) { context.ModelState.AddModelError(entry.Key, entry.Value.First()); } } } public void OnActionExecuted(ActionExecutedContext context) { // 无需处理后续逻辑 } }
3. 注册自定义过滤器
在Program.cs的控制器配置中添加这个过滤器:
builder.Services.AddControllers(options => { options.Filters.Add<RequiredPropertyValidationFilter>(); }) .AddJsonOptions(options => { options.JsonSerializerOptions.RequiredPropertyHandling = JsonRequiredPropertyHandling.Ignore; options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.PropertyNameCaseInsensitive = true; });
配置完成后,再发送缺失Summary的请求,System.Text.Json就会返回和Newtonsoft.Json完全一致的错误提示:
{ "errors": { "Summary": [ "The Summary field is required." ] } }
内容的提问来源于stack exchange,提问作者Eduardo Brites
相关产品推荐
相关产品推荐

