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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 17:24:58