NSwag icon indicating copy to clipboard operation
NSwag copied to clipboard

Array of `oneOf` leads to untyped list in C# if items schema is not inlined

Open minichma opened this issue 6 months ago • 0 comments

Problem Description

In my OpenAPI spec I have a response that is an array of oneOfs:

"schema": {
    "type": "array",
    "items": {
        "oneOf": [
            { "$ref": "#/components/schemas/BaseSomething" },
            { "$ref": "#/components/schemas/BusinessSomething" },
            { "$ref": "#/components/schemas/OtherSomething" }
        ]
    }
}

The type name generated for the respone is ICollection<BaseSomething>, which makes sense.

However, if the array's items reference a component or file instead of defining the oneOf inline, then the return type is a weaker typed ICollection<Anonymous>, which is less useful and doesn't even compile that way.

...
"schema": {
    "type": "array",
    "items": {
        "$ref": "#/components/schemas/ValidationSomething"
    }
}
...
"components": {
    "schemas": {
        "ValidationSomething": {
            "oneOf": [
                { "$ref": "#/components/schemas/BaseSomething" },
                { "$ref": "#/components/schemas/BusinessSomething" },
                { "$ref": "#/components/schemas/OtherSomething" }
            ]
        }
    }
}

Expected Behavior

I would expect the C# generator to generate the same type name in both cases. Even if the array item type references a component, I would expect the type name to be resolved as if it would be inlined. In the given example I would expect a return type of ICollection<BaseSomething> in both cases.

Complete Test Case

Here is a complete test:

        [Fact]
        public async Task Test()
        {
            // Arrange
            var json = """
            {
              "openapi": "3.0.0",
              "paths": {
                "/v1/get1": {
                  "get": {
                    "operationId": "Get1",
                    "responses": {
                      "200": {
                        "content": {
                          "application/json": {
                            "schema": {
                              "type": "array",
                              "items": {
                                "oneOf": [
                                  { "$ref": "#/components/schemas/BaseSomething" },
                                  { "$ref": "#/components/schemas/BusinessSomething" },
                                  { "$ref": "#/components/schemas/OtherSomething" }
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "/v1/get2": {
                  "get": {
                    "operationId": "Get2",
                    "responses": {
                      "200": {
                        "content": {
                          "application/json": {
                            "schema": {
                              "type": "array",
                              "items": {
                                "$ref": "#/components/schemas/ValidationSomething"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }, 
              "components": {
                "schemas": {
                  "BaseSomething": {
                    "properties": {
                      "discriminator": {
                        "type": "string",
                        "nullable": true
                      }
                    }
                  },
                  "BusinessSomething": {
                    "allOf": [
                      { "$ref": "#/components/schemas/BaseSomething" },
                      {
                        "type": "object",
                        "additionalProperties": false,
                        "properties": {
                            "userId": {
                                "type": "string",
                                "nullable": true
                            }
                        }
                      }
                    ]
                  },
                  "OtherSomething": {
                    "allOf": [
                      { "$ref": "#/components/schemas/BaseSomething" },
                      {
                        "type": "object",
                        "additionalProperties": false,
                        "properties": {
                            "otherId": {
                                "type": "string",
                                "nullable": true
                            }
                        }
                      }
                    ]
                  },
                  "ValidationSomething": {
                    "oneOf": [
                      { "$ref": "#/components/schemas/BaseSomething" },
                      { "$ref": "#/components/schemas/BusinessSomething" },
                      { "$ref": "#/components/schemas/OtherSomething" }
                    ]
                  }
                }
              }
            }
            """;

            var document = await OpenApiDocument.FromJsonAsync(json);

            var ops = document.Paths.Select(path => path.Value.Single().Value);

            var responseSchemas = ops.Select(op => op.Responses.Single().Value.Schema);
            var componentSchema = (document.Components.Schemas["ValidationSomething"]);

            // Act
            var settings = new CSharpClientGeneratorSettings { ClassName = "MyClass" };
            var generator = new CSharpClientGenerator(document, settings);

            var responseTypeNames = responseSchemas.Select(schema => generator.GetTypeName(schema, false, "Test")).ToList();
            var componentTypeName = generator.GetTypeName(componentSchema, false, "Component");

            Assert.All(responseTypeNames, typeName =>
            {
                Assert.Equal("System.Collections.Generic.ICollection<BaseSomething>", typeName);
            });

            Assert.Equal("BaseSomething", componentTypeName);
        }

Might be related to #1652, #2970

minichma avatar Aug 23 '24 10:08 minichma