Description
openedon Feb 18, 2024
When using inheritance using allOf
, Kiota generates multiple identical classes for the same component schema. As a result, the types are incompatible when trying to consume the C# code generated by Kiota.
I've tried to create a minimal repro, but the OAS is still quite large. The full file is provided below. At a high level, the structure contains the following base component schema:
"dataInResponse": {
"required": [
"id",
"type"
],
"type": "object",
"properties": {
"type": {
"minLength": 1,
"type": "string"
},
"id": {
"minLength": 1,
"type": "string"
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "type",
"mapping": {
"tags": "#/components/schemas/tagDataInResponse",
"todoItems": "#/components/schemas/todoItemDataInResponse"
}
},
"x-abstract": true
}
with derived schemas tagDataInResponse
and todoItemDataInResponse
, ie:
"todoItemDataInResponse": {
"allOf": [
{
"$ref": "#/components/schemas/dataInResponse"
},
{
"type": "object",
"properties": {
"attributes": {
"allOf": [
{
"$ref": "#/components/schemas/todoItemAttributesInResponse"
}
]
},
"relationships": {
"allOf": [
{
"$ref": "#/components/schemas/todoItemRelationshipsInResponse"
}
]
}
},
"additionalProperties": false
}
],
"additionalProperties": false
}
These schemas are used from two GET endpoints, one returning a collection and the other a singular item. The response schema in both cases contains a data
property (derived schema reference, or an array of that) and an included
property (array of base schema reference).
The first endpoint (paths./api/todoItems.get
) uses the following response schema:
"todoItemCollectionResponseDocument": {
"required": [
"data"
],
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/todoItemDataInResponse"
}
},
"included": {
"type": "array",
"items": {
"$ref": "#/components/schemas/dataInResponse"
}
}
},
"additionalProperties": false
}
And the second endpoint (paths./api/todoItems/{id}.get
) uses the following schema:
"todoItemPrimaryResponseDocument": {
"required": [
"data"
],
"type": "object",
"properties": {
"data": {
"allOf": [
{
"$ref": "#/components/schemas/todoItemDataInResponse"
}
]
},
"included": {
"type": "array",
"items": {
"$ref": "#/components/schemas/dataInResponse"
}
}
},
"additionalProperties": false
}
I would have expected Kiota to generate the base class DataInResponse
, with the two derived classes TodoItemDataInResponse
and TagDataInResponse
. What happens is that Kiota also generates the derived class TodoItems
, whose content is identical to TodoItemDataInResponse
. The unexpected TodoItems
class is only used by the singular endpoint.
As a result, it's not possible to define a method that takes a parameter of type TodoItemDataInResponse
and is called with the response from both endpoints. I would have expected to be able to write:
TodoItemPrimaryResponseDocument? getSingleResponse = await client.Api.TodoItems["1"].GetAsync();
PrintTodoItem(getSingleResponse!.Data!, getSingleResponse.Included);
TodoItemCollectionResponseDocument? getMultiResponse = await client.Api.TodoItems.GetAsync();
foreach (TodoItemDataInResponse todoItem in getMultiResponse!.Data!)
{
PrintTodoItem(todoItem, getMultiResponse.Included);
}
static void PrintTodoItem(TodoItemDataInResponse todoItem, ICollection<DataInResponse>? included)
{
// ...
}
Instead, the code for method PrintTodoItem
needs to be duplicated, because the types are incompatible:
TodoItemPrimaryResponseDocument? getSingleResponse = await client.Api.TodoItems["1"].GetAsync();
PrintTodoItem1(getSingleResponse!.Data!, getSingleResponse.Included);
TodoItemCollectionResponseDocument? getMultiResponse = await client.Api.TodoItems.GetAsync();
foreach (TodoItemDataInResponse todoItem in getMultiResponse!.Data!)
{
PrintTodoItem2(todoItem, getMultiResponse.Included);
}
static void PrintTodoItem1(TodoItems todoItem, ICollection<DataInResponse>? included)
{
// ...
}
static void PrintTodoItem2(TodoItemDataInResponse todoItem, ICollection<DataInResponse>? included)
{
// ...
}
Additionally, because there are duplicate types, it's unclear for consumers of the API what to upcast/type-check for when looping over the entries in included
.
When using NSwag to generate the client, types appear as expected, which makes me believe the OAS is correct.
Expand to view the full OAS file
{
"openapi": "3.0.1",
"info": {
"title": "JsonApiDotNetCoreExample",
"version": "1.0"
},
"servers": [
{
"url": "https://localhost:44340"
}
],
"paths": {
"/api/todoItems": {
"get": {
"tags": [
"todoItems"
],
"summary": "Retrieves a collection of todoItems.",
"operationId": "getTodoItemCollection",
"parameters": [
{
"name": "query",
"in": "query",
"description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
"example": ""
}
}
],
"responses": {
"200": {
"description": "Successfully returns the found todoItems, or an empty array if none were found.",
"content": {
"application/vnd.api+json": {
"schema": {
"$ref": "#/components/schemas/todoItemCollectionResponseDocument"
}
}
}
},
"400": {
"description": "The query string is invalid."
}
}
}
},
"/api/todoItems/{id}": {
"get": {
"tags": [
"todoItems"
],
"summary": "Retrieves an individual todoItem by its identifier.",
"operationId": "getTodoItem",
"parameters": [
{
"name": "id",
"in": "path",
"description": "The identifier of the todoItem to retrieve.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "query",
"in": "query",
"description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
"example": ""
}
}
],
"responses": {
"200": {
"description": "Successfully returns the found todoItem.",
"content": {
"application/vnd.api+json": {
"schema": {
"$ref": "#/components/schemas/todoItemPrimaryResponseDocument"
}
}
}
},
"400": {
"description": "The query string is invalid."
},
"404": {
"description": "The todoItem does not exist."
}
}
}
}
},
"components": {
"schemas": {
"dataInResponse": {
"required": [
"id",
"type"
],
"type": "object",
"properties": {
"type": {
"minLength": 1,
"type": "string"
},
"id": {
"minLength": 1,
"type": "string"
}
},
"additionalProperties": false,
"discriminator": {
"propertyName": "type",
"mapping": {
"tags": "#/components/schemas/tagDataInResponse",
"todoItems": "#/components/schemas/todoItemDataInResponse"
}
},
"x-abstract": true
},
"tagAttributesInResponse": {
"type": "object",
"properties": {
"name": {
"minLength": 1,
"type": "string"
}
},
"additionalProperties": false
},
"tagDataInResponse": {
"allOf": [
{
"$ref": "#/components/schemas/dataInResponse"
},
{
"type": "object",
"properties": {
"attributes": {
"allOf": [
{
"$ref": "#/components/schemas/tagAttributesInResponse"
}
]
},
"relationships": {
"allOf": [
{
"$ref": "#/components/schemas/tagRelationshipsInResponse"
}
]
}
},
"additionalProperties": false
}
],
"additionalProperties": false
},
"tagIdentifier": {
"required": [
"id",
"type"
],
"type": "object",
"properties": {
"type": {
"$ref": "#/components/schemas/tagResourceType"
},
"id": {
"minLength": 1,
"type": "string"
}
},
"additionalProperties": false
},
"tagRelationshipsInResponse": {
"type": "object",
"properties": {
"todoItems": {
"allOf": [
{
"$ref": "#/components/schemas/toManyTodoItemInResponse"
}
]
}
},
"additionalProperties": false
},
"tagResourceType": {
"enum": [
"tags"
],
"type": "string",
"additionalProperties": false
},
"toManyTagInResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/tagIdentifier"
}
}
},
"additionalProperties": false
},
"toManyTodoItemInResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/todoItemIdentifier"
}
}
},
"additionalProperties": false
},
"todoItemAttributesInResponse": {
"type": "object",
"properties": {
"description": {
"type": "string"
}
},
"additionalProperties": false
},
"todoItemCollectionResponseDocument": {
"required": [
"data"
],
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/todoItemDataInResponse"
}
},
"included": {
"type": "array",
"items": {
"$ref": "#/components/schemas/dataInResponse"
}
}
},
"additionalProperties": false
},
"todoItemDataInResponse": {
"allOf": [
{
"$ref": "#/components/schemas/dataInResponse"
},
{
"type": "object",
"properties": {
"attributes": {
"allOf": [
{
"$ref": "#/components/schemas/todoItemAttributesInResponse"
}
]
},
"relationships": {
"allOf": [
{
"$ref": "#/components/schemas/todoItemRelationshipsInResponse"
}
]
}
},
"additionalProperties": false
}
],
"additionalProperties": false
},
"todoItemIdentifier": {
"required": [
"id",
"type"
],
"type": "object",
"properties": {
"type": {
"$ref": "#/components/schemas/todoItemResourceType"
},
"id": {
"minLength": 1,
"type": "string"
}
},
"additionalProperties": false
},
"todoItemPrimaryResponseDocument": {
"required": [
"data"
],
"type": "object",
"properties": {
"data": {
"allOf": [
{
"$ref": "#/components/schemas/todoItemDataInResponse"
}
]
},
"included": {
"type": "array",
"items": {
"$ref": "#/components/schemas/dataInResponse"
}
}
},
"additionalProperties": false
},
"todoItemRelationshipsInResponse": {
"type": "object",
"properties": {
"tags": {
"allOf": [
{
"$ref": "#/components/schemas/toManyTagInResponse"
}
]
}
},
"additionalProperties": false
},
"todoItemResourceType": {
"enum": [
"todoItems"
],
"type": "string",
"additionalProperties": false
}
}
}
}
I'm using the next command to generate the client code:
dotnet kiota generate --language CSharp --class-name ApiClient --namespace-name GeneratedClient --output ./GeneratedClient --clean-output --clear-cache --openapi ..\JsonApiDotNetCoreExample\GeneratedSwagger\JsonApiDotNetCoreExample.json
Used versions:
- .NET 8 SDK v8.0.201 on Windows 11 23H2
- microsoft.openapi.kiota global tool v1.11.1 (latest available)
- Latest available NuGet package versions, as of today:
<PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.7.9" /> <PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.3.6" /> <PackageReference Include="Microsoft.Kiota.Serialization.Form" Version="1.1.3" /> <PackageReference Include="Microsoft.Kiota.Serialization.Json" Version="1.1.5" /> <PackageReference Include="Microsoft.Kiota.Serialization.Multipart" Version="1.1.2" /> <PackageReference Include="Microsoft.Kiota.Serialization.Text" Version="1.1.2" />
Metadata
Assignees
Labels
Type
Projects
Status
Done ✔️