NSwag生成客户端JSON反序列化时DTO读取返回空值问题
.NET8 Minimal API 结合 NSwag 客户端反序列化问题解析
问题背景
我在研究.NET8、Minimal API与整洁架构,现有一个登录接口在Swagger UI和Postman中均可正常返回AuthResponseDto数据:
app.MapPost("/user/login", async (HttpContext httpContext, [FromBody] LoginDto loginDto, [FromServices] IConfiguration conf, [FromServices] UserDataBackupDbContext db, [FromServices] UserManager<IdentityUser> userManager) => { try { {... shortened ...} var response = new AuthResponseDto { Id = user.Id, UserName = user.UserName ?? Constants.UnknownValue, Token = accessToken }; return TypedResults.Ok<AuthResponseDto>(response); } catch (Exception e) { return Results.BadRequest<string>(e.Message); } }).WithName("UserLogin") .Produces<AuthResponseDto>(StatusCodes.Status200OK) .Produces<string>(StatusCodes.Status400BadRequest) .AllowAnonymous();
但通过NSwag生成的客户端调用UserLoginAsync时,响应内容长度显示为0,反序列化后得到的是AuthResponseDto的默认实例(所有属性为默认值)。生成的客户端核心代码如下:
public virtual async Task<AuthResponseDto> UserLoginAsync(LoginDto loginDto, CancellationToken cancellationToken = default) { if (loginDto == null) throw new ArgumentNullException("loginDto"); var client_ = _httpClient; var disposeClient_ = false; try { using (var request_ = new HttpRequestMessage()) { var json_ = JsonSerializer.SerializeToUtf8Bytes(loginDto, _settings.Value); var content_ = new ByteArrayContent(json_); content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; request_.Method = HttpMethod.Post; request_.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); var urlBuilder_ = new StringBuilder(); if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); urlBuilder_.Append("user/login"); await PrepareRequestAsync(client_, request_, urlBuilder_, cancellationToken).ConfigureAwait(false); var url_ = urlBuilder_.ToString(); request_.RequestUri = new Uri(url_, UriKind.RelativeOrAbsolute); await PrepareRequestAsync(client_, request_, url_, cancellationToken).ConfigureAwait(false); var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); var disposeResponse_ = true; try { var headers_ = new Dictionary<string, IEnumerable<string>>(); foreach (var item_ in response_.Headers) headers_[item_.Key] = item_.Value; if (response_.Content != null && response_.Content.Headers != null) { foreach (var item_ in response_.Content.Headers) headers_[item_.Key] = item_.Value; } await ProcessResponseAsync(client_, response_, cancellationToken).ConfigureAwait(false); var status_ = (int)response_.StatusCode; if (status_ == 200) { var objectResponse_ = await ReadObjectResponseAsync<AuthResponseDto>(response_, headers_, cancellationToken).ConfigureAwait(false); if (objectResponse_.Object == null) { throw new UserDataBackupApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); } return objectResponse_.Object; } else if (status_ == 400) { var objectResponse_ = await ReadObjectResponseAsync<string>(response_, headers_, cancellationToken).ConfigureAwait(false); if (objectResponse_.Object == null) { throw new UserDataBackupApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); } throw new UserDataBackupApiException<string>("A server side error occurred.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); } else { var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new UserDataBackupApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); } } finally { if (disposeResponse_) response_.Dispose(); } } } finally { if (disposeClient_) client_.Dispose(); } }
负责反序列化的核心方法如下(当前场景走else分支从流读取):
protected virtual async Task<ObjectResponseResult<T>> ReadObjectResponseAsync<T>(HttpResponseMessage response, IReadOnlyDictionary<string, IEnumerable<string>> headers, CancellationToken cancellationToken) { if (response == null || response.Content == null) { return new ObjectResponseResult<T>(default(T), string.Empty); } if (ReadResponseAsString) { var responseText = await response.Content.ReadAsStringAsync().ConfigureAwait(false); try { var typedBody = JsonSerializer.Deserialize<T>(responseText, JsonSerializerSettings); return new ObjectResponseResult<T>(typedBody, responseText); } catch (JsonException exception) { var message = "Could not deserialize the response body string as " + typeof(T).FullName + "."; throw new UserDataBackupApiException(message, (int)response.StatusCode, responseText, headers, exception); } } else { try { using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) { var typedBody = await JsonSerializer.DeserializeAsync<T>(responseStream, JsonSerializerSettings, cancellationToken).ConfigureAwait(false); return new ObjectResponseResult<T>(typedBody, string.Empty); } } catch (JsonException exception) { var message = "Could not deserialize the response body stream as " + typeof(T).FullName + "."; throw new UserDataBackupApiException(message, (int)response.StatusCode, string.Empty, headers, exception); } } }
疑问解答
1. 为什么反序列化后的对象没有填充响应中的值?
核心原因大概率是JSON序列化/反序列化配置不匹配或DTO定义问题:
- 检查
AuthResponseDto的属性是否带有setter:System.Text.Json无法反序列化只读属性(无setter的属性)。 - 检查字段名大小写匹配:API默认返回驼峰命名的JSON(如
userName),而DTO属性如果是PascalCase(如UserName),需要在反序列化时开启PropertyNameCaseInsensitive = true,否则无法匹配字段。 - 检查是否有字段名不匹配的情况:如果API返回的字段名和DTO属性名不一致,需要给DTO属性添加
[JsonPropertyName("xxx")]特性指定对应JSON字段名。 - 排查流读取问题:如果
ProcessResponseAsync方法中提前读取过响应流,会导致流位置偏移,后续读取时无法获取数据,可以临时注释该方法调用测试。
2. 为什么使用HttpCompletionOption.ResponseHeadersRead?手动改为ResponseContentRead后内容长度大于0且可读取字符串,但仍返回默认实例?
HttpCompletionOption.ResponseHeadersRead是NSwag默认生成的配置,它的作用是仅读取响应头就返回Task,不等待响应内容完全下载,这样可以提前处理头信息,提升性能,但需要确保后续代码正确读取响应内容流。- 改为
ResponseContentRead后内容长度正常,说明内容已经完整下载,但反序列化仍返回默认值,证明问题不在内容是否下载,而是反序列化本身的配置或DTO定义问题(即疑问1中的原因),和HttpCompletionOption无关。
3. 是否有OpenApi或NSwag配置项可解决该问题?
有多个配置项可以针对性解决:
API端(OpenApi配置)
- 确保Minimal API的OpenAPI元数据正确生成:可以显式添加
[ProducesResponseType(typeof(AuthResponseDto), StatusCodes.Status200OK)]特性,或者在Program.cs中配置Swagger时启用EnableAnnotations(),确保Schema包含AuthResponseDto的所有属性。 - 统一API的JSON序列化配置:在
Program.cs中配置System.Text.Json,确保返回的JSON格式和客户端反序列化配置匹配:builder.Services.AddControllers().AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; // 保持默认驼峰 options.JsonSerializerOptions.WriteIndented = false; });
NSwag客户端配置
- 在NSwag生成客户端时,开启大小写不敏感反序列化:在
nswag.json的jsonSerializerSettings中添加"propertyNameCaseInsensitive": true。 - 启用
ReadResponseAsString:在NSwag配置中设置readResponseAsString: true,让客户端先将响应读为字符串再反序列化,方便排查响应内容是否正确,也能避免流读取的潜在问题。 - 生成客户端时指定DTO的命名策略:在NSwag配置中设置
jsonSerializerSettings.propertyNamingPolicy为CamelCase,确保和API端的序列化策略一致。
内容的提问来源于stack exchange,提问作者Gottfried Lesigang
相关产品推荐
相关产品推荐

