.NET 6.0 JSON反序列化属性丢失 序列化配置引发登录异常
项目基于 .NET 6.0 开发,使用 PriceVM 类反序列化第三方接口返回的JSON数据后,Postman调用接口的输出结果中 Show、Ticket 字段缺失。
第三方接口原始返回JSON
未使用模型绑定反序列化时,接口返回的原始JSON结构如下:
{ "traceId": "fhregtkgregtireuiti3t", "data": { "airPrice":[ { "key":"kjewqerlewqwwq", "show":true, "showSpecified":true, "departure":"2022-06-28T19:08:00.000+01:00", "solution":[ { "ticket":false, "ticketSpecified":false, "origin":"JED", "exchange":false }, { "ticket":false, "ticketSpecified":false, "origin":"MND", "exchange":false } ] } ] } }
自定义实体类定义
对应JSON结构定义的实体类代码如下:
public class PriceVM{ [JsonProperty("traceId")] public string TraceId {get;set;} [JsonProperty("data")] public Data Data {get;set;} } public class Data { [JsonProperty("airPrice")] public IEnumerable<AirPrice> AirPrice {get;set;} } public class AirPrice{ [JsonProperty("airPrice")] public string Key {get;set;} [JsonProperty("show")] public bool Show {get;set;} [JsonProperty("showSpecified")] public bool ShowSpecified {get;set;} [JsonProperty("departure")] public DateTime Departure {get;set;} [JsonProperty("solution")] public List<Solution> Solution {get;set;} } public class Solution{ [JsonProperty("ticket")] public bool Ticket {get;set;} [JsonProperty("ticketSpecified")] public bool TicketSpecified {get;set;} [JsonProperty("origin")] public string Origin {get;set;} [JsonProperty("exchange")] public bool Exchange {get;set;} }
接口调用逻辑
调用第三方API、反序列化返回结果的代码如下:
public async Task<PriceVM> Handle(PriceQuery request) { var response= await client.PostAsJsonAsync<ADT>("url",request); response.EnsureSuccessStatusCode(); string responseBody = await response.Content.ReadAsStringAsync(); var airPrice = JsonConvert.DeserializeObject<PriceVM>(responseBody, new JsonSerializerSettings { DateParseHandling = DateParseHandling.None, }); return airPrice; }
控制器端点定义:
[HttpPost] [Route("price")] public async Task<ActionResult<PriceVM>> AirPrice( [FromBody] ADT dto) { var result = await pricesrvice.Handle(dto); return Ok(result); }
异常表现
使用强类型反序列化后,接口返回结果缺失Show、Ticket属性,返回内容如下:
{ "traceId": "fhregtkgregtireuiti3t", "data": { "airPrice":[ { "key":"kjewqerlewqwwq", "showSpecified":true, "departure":"2022-06-28T19:08:00.000+01:00", "solution":[ { "ticketSpecified":false, "origin":"JED", "exchange":false }, { "ticketSpecified":false, "origin":"MND", "exchange":false } ] } ] } }
调试时确认控制器内的result对象实际包含Show、Ticket属性和对应值;如果使用非强类型方式反序列化,输出结果可以包含所有字段:
var airPrice = JsonConvert.DeserializeObject(responseBody, new JsonSerializerSettings { DateParseHandling = DateParseHandling.None, });
已尝试方案与副作用
排查确认问题与属性名+Specified格式的配对属性有关(例如ticket/ticketSpecified、show/showSpecified),因此在Program.cs中添加了如下NewtonsoftJson配置:
builder.Services.AddControllers(options => options.Filters.Add<ApiExceptionFilterAttribute>()) .AddFluentValidation() .AddNewtonsoftJson(options => { options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; // 新增配置 options.SerializerSettings.ContractResolver = new DefaultContractResolver() { IgnoreIsSpecifiedMembers = false, }; });
添加该配置后,价格查询接口可以正常返回所有属性,但会导致UI端登录功能失效,开发、生产环境控制台无任何错误输出;注释掉上述ContractResolver配置段后,登录功能恢复正常。
- 字段缺失的核心原因是Newtonsoft.Json默认的
xxxSpecified约定规则:当类中存在和布尔属性同名、后缀为Specified的布尔属性时,序列化阶段会自动检查xxxSpecified的值,如果该值为false,就会跳过对应xxx属性的序列化。你的场景中ticketSpecified值为false,因此序列化时ticket字段被自动忽略。 - 全局配置导致登录失败的原因是直接new了默认的
DefaultContractResolver,覆盖了系统原有的契约解析配置(包括默认驼峰命名策略、身份认证模型的序列化规则),导致登录等其他接口的返回字段名、序列化逻辑发生无感知变化,前端无法正确读取认证相关字段,最终登录失败。
不要全局替换ContractResolver,采用小范围配置的方式解决问题,避免影响其他业务接口:
方案1:类级别特性标注(推荐)
直接在涉及Specified字段的实体类上添加[JsonObject]特性,指定仅这两个类序列化时忽略Specified成员检查,不需要修改全局配置,完全不影响登录等其他接口。注意同时修正AirPrice类中Key属性的映射错误(原代码错误映射到了airPrice字段):
[JsonObject(IgnoreIsSpecifiedMembers = true)] public class AirPrice{ [JsonProperty("key")] // 修正原映射错误 public string Key {get;set;} [JsonProperty("show")] public bool Show {get;set;} [JsonProperty("showSpecified")] public bool ShowSpecified {get;set;} [JsonProperty("departure")] public DateTime Departure {get;set;} [JsonProperty("solution")] public List<Solution> Solution {get;set;} } [JsonObject(IgnoreIsSpecifiedMembers = true)] public class Solution{ [JsonProperty("ticket")] public bool Ticket {get;set;} [JsonProperty("ticketSpecified")] public bool TicketSpecified {get;set;} [JsonProperty("origin")] public string Origin {get;set;} [JsonProperty("exchange")] public bool Exchange {get;set;} }
方案2:定义专用输出DTO
额外定义接口输出专用的DTO类,去掉ShowSpecified、TicketSpecified这类第三方接口返回的冗余字段,反序列化得到第三方数据后,手动映射到输出DTO再返回,从根源上避免触发Specified命名约定,不需要调整任何序列化配置,对现有业务零侵入。
// 输出专用DTO,无Specified后缀字段 public class PriceVMOuput{ public string TraceId {get;set;} public DataOutput Data {get;set;} } public class DataOutput{ public IEnumerable<AirPriceOutput> AirPrice {get;set;} } public class AirPriceOutput{ public string Key {get;set;} public bool Show {get;set;} public DateTime Departure {get;set;} public List<SolutionOutput> Solution {get;set;} } public class SolutionOutput{ public bool Ticket {get;set;} public string Origin {get;set;} public bool Exchange {get;set;} }
内容的提问来源于stack exchange,提问作者AG Mohamed

