NSwag: Bug in Error serialization openapi2csclient

my yaml:

    Error:
      type: object
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
        description:
          type: string
    Errors:
      type: object
      properties:
        errors:
          type: array
          items:
            $ref: '#/components/schemas/Error'
    ErrorCode:
      description: Enum of errors
      type: string
      enum:
        - NOTFOUND
        - GENERAL_EXCEPTION

service I am calling returns this:

{
    "errors": [
        {
            "code": "GENERAL_EXCEPTION",
            "description": "03396370-ddc4-41c4-ae03-ed07249a9d1b - Not supported yet."
        }
    ]
}

I cannot get serialized object Errors in proper format with current error handling. This is current generated c# code

     if (status_ == "500") 
                        {
                            var objectResponse_ = await ReadJsonObjectResponseAsync<Errors>(response_, headers_).ConfigureAwait(false);
                            throw new ApiException<Errors>("Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null);
                        }

objectResponse_ contains Errors object which is what I get from service I am calling but Text property is empty and the Errors object is not seriliazed correctly

this used to work in my previously generated code:

                       if (status_ == "500") 
                        {
                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); 
                            var result_ = default(Errors); 
                            try
                            {
                                result_ = Newtonsoft.Json.JsonConvert.DeserializeObject<Errors>(responseData_, _settings.Value);
                            } 
                            catch (System.Exception exception_) 
                            {
                                throw new SwaggerException("Could not deserialize the response body.", (int)response_.StatusCode, responseData_, headers_, exception_);
                            }
                            throw new SwaggerException<Errors>("Errors", (int)response_.StatusCode, responseData_, headers_, result_, null);
                        }

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 19 (15 by maintainers)

Most upvoted comments

@rars Oh, I see there’s a new NSwag template Client.Class.Constructor that you can override (so you don’t need any other *.liquid templates) to set the property - that’s probably the best approach.

@rars

There are several options.

The simplest is to set ReadResponseAsString in a partial method - which requires adding partial class declarations in another file and keeping it in-sync with the NSwag-generated clients. The ProcessResponse partial method is the best place to set ReadResponseAsString as it’s always called right before deserialization:

public partial class BooksClient
{
    partial void ProcessResponse(HttpClient client, HttpResponseMessage response)
    {
        this.ReadResponseAsString = false;
    }
}
public partial class VideosClient
{
    partial void ProcessResponse(HttpClient client, HttpResponseMessage response)
    {
        this.ReadResponseAsString = false;
    }
}
public partial class TeaClient
{
    partial void ProcessResponse(HttpClient client, HttpResponseMessage response)
    {
        this.ReadResponseAsString = false;
    }
}
// etc

Another approach, which uses MEDI, and is documented by Microsoft as the prescribed way for customizing registered typed-clients is to use ITypedHttpClientFactory.

Unfortunately because the ReadResponseAsString property isn’t attached to any superclass or interface we can’t use a common factory method for all of your typed clients - also you’ll need to reimplement ITypedHttpClientFactory - which is a pain. And I won’t go into details here.

I think the best approach here is to override the Liquid template that NSwag is using to generate a client class constructor that sets the this.ReadResponseAsString = false;

or just subclass all of the typed clients and set the property there:

class BooksClient2 : BooksClient
{
    public BooksClient( HttpClient httpClient ) : base( httpClient )
    {
        this.ReadResponseAsString = false;
    }
}

void RegisterClients(IServiceCollection serviceCollection)
{
    services.AddHttpClient<IBooksClient, BooksClient2>(
        httpClient => httpClient.BaseAddress = "https://books.com/api");`
}

…which is about the same amount of effort as using partial classes.

@RicoSuter I think the option to use ReadResponseAsString should be a configurable option in NSwag client generation options - as well as adding an optional interface ITypedClient that exposes common members to external consumers, such as the ReadResponseAsString property.

On the generated client class