Autorest生成客户端调用接口 抽象类列表序列化报错
问题描述
在行为测试中调用控制器接口时,使用Postman手动调用可正常运行,但使用Autorest生成的HttpClient调用时失败。已创建专用JsonConverter处理多态序列化逻辑,调用后返回400响应,内容如下:
{ "errors":{ "command":[ "The command field is required." ], "Sections[0].Title.0":[ "Could not create an instance of type Section. Type is an interface or abstract class and cannot be instantiated. Path 'Sections[0].Title', line 7, position 14." ], "Sections[1].Kind.1":[ "Could not create an instance of type Section. Type is an interface or abstract class and cannot be instantiated. Path 'Sections[1].Kind', line 14, position 13." ] }, "type":"https://tools.ietf.org/html/rfc7231#section-6.5.1", "title":"One or more validation errors occurred.", "status":400, "traceId":"0HMI4P0JALIN1" }
涉及的模型采用抽象类列表结构定义,相关代码如下:
public enum Kind { CoverPage, PageBreak } public abstract record Section(Kind Kind, string Name); public record PageBreak(string Name) : Section(Kind.PageBreak, Name) { public string DisplayedString => _displayedString; private static string _displayedString = "............"; } public record CoverPage(string Name, string Title, string SubTitleFirstLevel, string SubTitleSecondLevel) : Section(Kind.CoverPage, Name); public record CreateTemplateCommand { [Required] public string Name { get; init; } [Required] public IList<Section> Sections { get; init; } }
Autorest生成的请求内容与Postman中传入的JSON入参结构完全一致,请求体如下:
{ "Name": "My Name", "Sections": [ { "Title": "Blablabla", "SubTitleFirstLevel": "Sous-Titre 1", "SubTitleSecondLevel": "Sous-Titre 2", "Kind": "CoverPage", "Name": "Page de garde" }, { "Kind": "PageBreak", "Name": "Saut de page" } ] }
Autorest生成的接口调用代码如下:
public async Task<HttpOperationResponse> CreateTemplateWithHttpMessagesAsync(int enterpriseId, CreatePlaquetteTemplateCommand body, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken)) { if (body == null) { throw new ValidationException(ValidationRules.CannotBeNull, "body"); } if (body != null) { body.Validate(); } // Tracing bool _shouldTrace = ServiceClientTracing.IsEnabled; string _invocationId = null; if (_shouldTrace) { _invocationId = ServiceClientTracing.NextInvocationId.ToString(); Dictionary<string, object> tracingParameters = new Dictionary<string, object>(); tracingParameters.Add("enterpriseId", enterpriseId); tracingParameters.Add("body", body); tracingParameters.Add("cancellationToken", cancellationToken); ServiceClientTracing.Enter(_invocationId, this, "CreateTemplate", tracingParameters); } // Construct URL var _baseUrl = Client.BaseUri.AbsoluteUri; var _url = new System.Uri(new System.Uri(_baseUrl + (_baseUrl.EndsWith("/") ? "" : "/")), "api/plaquette/enterprises/{enterpriseId}/Templates").ToString(); _url = _url.Replace("{enterpriseId}", System.Uri.EscapeDataString(Microsoft.Rest.Serialization.SafeJsonConvert.SerializeObject(enterpriseId, Client.SerializationSettings).Trim('"'))); // Create HTTP transport objects var _httpRequest = new HttpRequestMessage(); HttpResponseMessage _httpResponse = null; _httpRequest.Method = new HttpMethod("POST"); _httpRequest.RequestUri = new System.Uri(_url); // Set Headers if (customHeaders != null) { foreach(var _header in customHeaders) { if (_httpRequest.Headers.Contains(_header.Key)) { _httpRequest.Headers.Remove(_header.Key); } _httpRequest.Headers.TryAddWithoutValidation(_header.Key, _header.Value); } } // Serialize Request string _requestContent = null; if(body != null) { _requestContent = Microsoft.Rest.Serialization.SafeJsonConvert.SerializeObject(body, Client.SerializationSettings); _httpRequest.Content = new StringContent(_requestContent, System.Text.Encoding.UTF8); _httpRequest.Content.Headers.ContentType =System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json-patch+json; charset=utf-8"); } // Set Credentials if (Client.Credentials != null) { cancellationToken.ThrowIfCancellationRequested(); await Client.Credentials.ProcessHttpRequestAsync(_httpRequest, cancellationToken).ConfigureAwait(false); } // Send Request if (_shouldTrace) { ServiceClientTracing.SendRequest(_invocationId, _httpRequest); } cancellationToken.ThrowIfCancellationRequested(); _httpResponse = await Client.HttpClient.SendAsync(_httpRequest, cancellationToken).ConfigureAwait(false); if (_shouldTrace) { ServiceClientTracing.ReceiveResponse(_invocationId, _httpResponse); } HttpStatusCode _statusCode = _httpResponse.StatusCode; cancellationToken.ThrowIfCancellationRequested(); string _responseContent = null; if ((int)_statusCode != 200 && (int)_statusCode != 201 && (int)_statusCode != 204) { var ex = new HttpOperationException(string.Format("Operation returned an invalid status code '{0}'", _statusCode)); if (_httpResponse.Content != null) { _responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); } else { _responseContent = string.Empty; } ex.Request = new HttpRequestMessageWrapper(_httpRequest, _requestContent); ex.Response = new HttpResponseMessageWrapper(_httpResponse, _responseContent); if (_shouldTrace) { ServiceClientTracing.Error(_invocationId, ex); } _httpRequest.Dispose(); if (_httpResponse != null) { _httpResponse.Dispose(); } throw ex; } // Create Result var _result = new HttpOperationResponse(); _result.Request = _httpRequest; _result.Response = _httpResponse; if (_shouldTrace) { ServiceClientTracing.Exit(_invocationId, _result); } return _result; }
相同入参下Postman调用正常,Autorest生成客户端调用返回400错误,提示无法实例化抽象类Section。
问题根因
核心问题出在Autorest生成代码的请求Content-Type配置错误:
- 生成的代码强制把请求Content-Type设置为
application/json-patch+json,而非标准的application/json。ASP.NET Core会根据请求Content-Type选择对应的输入格式化器:application/json-patch+json会走JSON Patch专用的格式化器,这个格式化器不会加载之前配置的、用于处理Section多态反序列化的自定义JsonConverter,只会用默认逻辑直接反序列化抽象类,自然会抛出无法实例化抽象/接口类型的错误。 - Postman调用时默认用
application/json作为Content-Type,会走配置了自定义多态转换器的JSON格式化器,所以反序列化正常。 - 响应里连带出现的
command字段必填错误,是JSON Patch格式化器解析请求体失败后,模型绑定找不到对应接口参数抛出的连锁错误,不是独立问题。
修复方案
- 不要直接修改生成的代码(重新生成会被覆盖),在Autorest的生成配置文件中覆写请求Content-Type为
application/json,从生成源头修正ContentType配置。 - 检查Autorest客户端的
SerializationSettings配置,把服务端使用的多态JsonConverter注册到该配置的Converters集合中,保证客户端序列化多态类型时的输出逻辑和服务端预期完全一致,避免不同序列化配置导致的结构差异。 - 修正ContentType后重新生成客户端发起调用,两个400错误会同时消失。
内容的提问来源于stack exchange,提问作者alpachinois
相关产品推荐
相关产品推荐

