Skip to content

Multiple identical classes are generated for a component schema with allOf inheritance #4191

Closed

Description

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" />
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

WIPgeneratorIssues or improvements relater to generation capabilities.help wantedIssue caused by core project dependency modules or librarytype:bugA broken experience

Type

No type

Projects

  • Status

    Done ✔️

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions