microsoft / kiota

OpenAPI based HTTP Client code generator
https://aka.ms/kiota/docs
MIT License
2.92k stars 203 forks source link

C# .NET HttpValidationProblem not working (or at least extremely unergonomic) #4862

Closed linde12 closed 4 months ago

linde12 commented 4 months ago

What are you generating using Kiota, clients or plugins?

API Client/SDK

In what context or format are you using Kiota?

Linux executable

Client library/SDK language

Csharp

Describe the bug

I'm generating towards an endpoint that returns a .NET HttpProblemResult or anything that produces a problem details response, but when i instead switch to the built-in ValidationProblem (e.g. return TypedResults.ValidationProblem(errors); from my endpoint) and annotate the endpoint with .ProducesValidationProblem(); Kiota produces an extremely unusable client.

The generated OpenAPI Schema (from the .NET application) is fine (see screenshot), and in the generated .NET client there is a ex.Errors which in turn contains an AdditionalData dictionary. This dictionary is not what you would expect though (Dictionary<string, string[]> of errors) but a dict containing one key ("errors") which is an Microsoft.Kiota.Abstractions.Serialization.UntypedArray

As it stands, by following the problem details standard and using the ValidationProblem provided by .NET, Kiota generates a pretty unusable or at least very unergonomic (i have not looked into UntypedArray) exception for non-200 OK situations.

Image

Expected behavior

I expect Kiota to follow the API Schema (see screenshot in description above) and generate a MyApi.Models.HttpValidationProblemDetails exception which contains a Errors (alternatively Errors.AdditionalData if thats some convention Kiota follows?) that is of type Dictionary<string, string[]>)

The OpenAPI Schema clearly states that errors is an object where each additionalProperty (every property in this case) is an array of strings.

How to reproduce

Create e.g. a .NET Minimal API endpoint that produces the schema above and make the endpoint throw return a TypedResults.ValidationProblem and "annotate" the route with the ProducesValidationProblem method. Generate a client and look at the HttpValidationProblemDetails.cs

Open API description file

Cannot provide since it is confidential, but would be willing to produce a more complete minimally reproducible example if this is written off as expected behavior.

Kiota Version

1.14

Latest Kiota version known to work for scenario above?(Not required)

No response

Known Workarounds

No response

Configuration

No response

Debug output

No response

Other information

No response

baywet commented 4 months ago

Hi @linde12 Thanks for using kiota and for reaching out. Can you please share more of your OAS description (in text format) to include the path/request/responses so we have more context here? Can you also share the C# of what's being generated today, and what you'd expect instead? Thanks!

linde12 commented 4 months ago

Hi @baywet I was celebrating a Swedish holiday so excuse the delay šŸ˜„

Here is a full OAS:

{
  "openapi": "3.0.1",
  "info": {
    "title": "ex",
    "version": "1.0"
  },
  "paths": {
    "/weatherforecast": {
      "get": {
        "tags": [
          "ex"
        ],
        "operationId": "GetWeatherForecast",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WeatherForecast"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/HttpValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DateOnly": {
        "type": "object",
        "properties": {
          "year": {
            "type": "integer",
            "format": "int32"
          },
          "month": {
            "type": "integer",
            "format": "int32"
          },
          "day": {
            "type": "integer",
            "format": "int32"
          },
          "dayOfWeek": {
            "$ref": "#/components/schemas/DayOfWeek"
          },
          "dayOfYear": {
            "type": "integer",
            "format": "int32",
            "readOnly": true
          },
          "dayNumber": {
            "type": "integer",
            "format": "int32",
            "readOnly": true
          }
        },
        "additionalProperties": false
      },
      "DayOfWeek": {
        "enum": [
          0,
          1,
          2,
          3,
          4,
          5,
          6
        ],
        "type": "integer",
        "format": "int32"
      },
      "HttpValidationProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "nullable": true
          }
        },
        "additionalProperties": { }
      },
      "WeatherForecast": {
        "type": "object",
        "properties": {
          "date": {
            "$ref": "#/components/schemas/DateOnly"
          },
          "temperatureC": {
            "type": "integer",
            "format": "int32"
          },
          "summary": {
            "type": "string",
            "nullable": true
          },
          "temperatureF": {
            "type": "integer",
            "format": "int32",
            "readOnly": true
          }
        },
        "additionalProperties": false
      }
    }
  }
}

This is generated from a standard WebAPI application, and my endpoint looks like so:

static Results<Ok<WeatherForecast[]>, ValidationProblem> GetWeatherForecast()
{
  if (Random.Shared.Next(0, 2) == 0)
  {
    var errors = new Dictionary<string, string[]>
    {
        { "username", new[] { "Required" } }
    };
    return TypedResults.ValidationProblem(errors);
  }

  var forecast = Enumerable.Range(1, 5).Select(index =>
      new WeatherForecast
      (
          DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
          Random.Shared.Next(-20, 55),
          "Freezing"
      ))
      .ToArray();

  return TypedResults.Ok(forecast);
}

app.MapGet("/weatherforecast", GetWeatherForecast)
  .WithName("GetWeatherForecast")
  .WithOpenApi()
  .ProducesValidationProblem();

and 50% of the time it will produce a validation problem response containing an error like so (retrieved with curl):

{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"username":["Required"]}}

Then i am using the generated client from Kiota like so:

using ApiSdk;
using Microsoft.Kiota.Abstractions.Authentication;
using Microsoft.Kiota.Http.HttpClientLibrary;

var adapter = new HttpClientRequestAdapter(new AnonymousAuthenticationProvider());
adapter.BaseUrl = "http://localhost:5117";
var client = new ApiClient(adapter);

try
{
  var res = await client.Weatherforecast.GetAsync();
}
catch (ApiSdk.Models.HttpValidationProblemDetails problem)
{
  var errors = problem.Errors; // type is HttpValidationProblemDetails_errors, only contains a "AdditionalData" field
  foreach (var error in errors.AdditionalData)
  {
    Console.WriteLine($"Error: {error.Key} - {error.Value}");
  }
}

This piece of code will print the following when an error occurs:

Error: username - Microsoft.Kiota.Abstractions.Serialization.UntypedArray

I'd expect to be able to do the following (according to OAS):

  var errors = problem.Errors; // type is HttpValidationProblemDetails_errors, only contains a "AdditionalData" field
  foreach (var error in errors.AdditionalData)
  {
    Console.WriteLine($"Error: {error.Key}");
    foreach (var message in error.Value)
    {
      Console.WriteLine($"{message}"); // print every error message for the given key
    }
  }

but this does not work since error.Value is upcasted to object and in reality is UntypedArray

linde12 commented 4 months ago

The OpenAPI Schema was generated using the standard UseSwagger() you get from dotnet new webapi and the returned ValidationProblem is from Microsoft.AspNetCore

I'm not sure how i should work with validation errors here.

andrueastman commented 4 months ago

At the moment, the type information for the isn't used by the deserializer for the items in the additionalData. So they are represented using UntypedNodes as documented at https://learn.microsoft.com/en-us/openapi/kiota/serialization?tabs=csharp#untyped-node.

Any chance you are able to print out the information with the code below?

foreach (var error in errors.AdditionalData)
{
    Console.WriteLine($"Error: {error.Key}");
    if (error.Value is UntypedArray arrayValue)
    {
        foreach (var message in arrayValue.GetValue())
        {
            if (message is UntypedString messageString)
                Console.WriteLine($"{messageString.GetValue()}"); // print every error message for the given key
        }
    }
}
linde12 commented 4 months ago

@andrueastman Ah, that explains it. Is this something that is being actively worked or something that's planned? We ended up with a similar workaround to reach the wanted values, it was just a bit cumbersome and we had to do some digging to understand how to approach this.

Thank you for prompt replies šŸ™

baywet commented 4 months ago

Hi everyone, Thanks for all the additional information here. I'm going to go ahead and close this as a duplicate of #62 since the resolution requires support of dictionaries in kiota, which requires support of OAS 3.1 with openapi.net so both the description generation on asp.net side, and the code generation can work hand in hand. Additional, if you read the conversation over there, you'll see that people have been complaining about the same scenario (validation errors from asp.net)