Skip to content

Generated OpenAPI schema is invalid for nested recursive polymorphic type, even without arrays #61722

@andrewimcclement

Description

@andrewimcclement

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

app.MapGet("/foo",
                 () =>
                 {
                     Foo foo = new Foo1(new Bar1());
                     return TypedResults.Ok(foo);
                 })
         .WithName("GetFoo");

[JsonDerivedType(typeof(Foo1), "foo1")]
[JsonDerivedType(typeof(Foo2), "foo2")]
public abstract record Foo(Bar Bar);
public sealed record Foo1(Bar Bar, string Haha = "boo") : Foo(Bar);
public sealed record Foo2(Bar Bar, int Musketeer = 3) : Foo(Bar);

[JsonDerivedType(typeof(Bar1), "bar1")]
[JsonDerivedType(typeof(Bar2), "bar2")]
public abstract record Bar;
public sealed record Bar1(string Hi = "Hi") : Bar;
public sealed record Bar2(Bar Left, Bar Right) : Bar;

generates a deeply nested schema which fails at a certain depth (note that Bar2 is a recursive polymorphic type).

Generated OpenAPI schema - note it gives up after a certain depth

This can be generated by running the https profile.

{
  "openapi": "3.0.1",
  "info": {
    "title": "Issue | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://localhost:7242/"
    }
  ],
  "paths": {
    "/foo": {
      "get": {
        "tags": [
          "Issue"
        ],
        "operationId": "GetFoo",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Foo"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Foo": {
        "required": [
          "$type"
        ],
        "type": "object",
        "anyOf": [
          {
            "$ref": "#/components/schemas/FooFoo1"
          },
          {
            "$ref": "#/components/schemas/FooFoo2"
          }
        ],
        "discriminator": {
          "propertyName": "$type",
          "mapping": {
            "foo1": "#/components/schemas/FooFoo1",
            "foo2": "#/components/schemas/FooFoo2"
          }
        }
      },
      "FooFoo1": {
        "required": [
          "bar"
        ],
        "properties": {
          "$type": {
            "enum": [
              "foo1"
            ],
            "type": "string"
          },
          "haha": {
            "type": "string",
            "default": "boo"
          },
          "bar": {
            "required": [
              "$type"
            ],
            "type": "object",
            "anyOf": [
              {
                "properties": {
                  "$type": {
                    "enum": [
                      "bar1"
                    ],
                    "type": "string"
                  },
                  "hi": {
                    "type": "string",
                    "default": "Hi"
                  }
                }
              },
              {
                "required": [
                  "left",
                  "right"
                ],
                "properties": {
                  "$type": {
                    "enum": [
                      "bar2"
                    ],
                    "type": "string"
                  },
                  "left": {
                    "required": [
                      "$type"
                    ],
                    "type": "object",
                    "anyOf": [
                      {
                        "properties": {
                          "$type": {
                            "enum": [
                              "bar1"
                            ],
                            "type": "string"
                          },
                          "hi": {
                            "type": "string",
                            "default": "Hi"
                          }
                        }
                      },
                      {
                        "required": [
                          "left",
                          "right"
                        ],
                        "properties": {
                          "$type": {
                            "enum": [
                              "bar2"
                            ],
                            "type": "string"
                          },
                          "left": {
                            "discriminator": {
                              "propertyName": "$type",
                              "mapping": {
                                "bar1": "#/components/schemas/BarBar1",
                                "bar2": "#/components/schemas/BarBar2"
                              }
                            }
                          },
                          "right": {
                            "required": [
                              "$type"
                            ],
                            "type": "object",
                            "anyOf": [
                              {
                                "properties": {
                                  "$type": {
                                    "enum": [
                                      "bar1"
                                    ],
                                    "type": "string"
                                  },
                                  "hi": {
                                    "type": "string",
                                    "default": "Hi"
                                  }
                                }
                              },
                              {
                                "required": [
                                  "left",
                                  "right"
                                ],
                                "properties": {
                                  "$type": {
                                    "enum": [
                                      "bar2"
                                    ],
                                    "type": "string"
                                  },
                                  "left": {
                                    "discriminator": {
                                      "propertyName": "$type",
                                      "mapping": {
                                        "bar1": "#/components/schemas/BarBar1",
                                        "bar2": "#/components/schemas/BarBar2"
                                      }
                                    }
                                  },
                                  "right": {
                                    "discriminator": {
                                      "propertyName": "$type",
                                      "mapping": {
                                        "bar1": "#/components/schemas/BarBar1",
                                        "bar2": "#/components/schemas/BarBar2"
                                      }
                                    }
                                  }
                                }
                              }
                            ],
                            "discriminator": {
                              "propertyName": "$type",
                              "mapping": {
                                "bar1": "#/components/schemas/BarBar1",
                                "bar2": "#/components/schemas/BarBar2"
                              }
                            }
                          }
                        }
                      }
                    ],
                    "discriminator": {
                      "propertyName": "$type",
                      "mapping": {
                        "bar1": "#/components/schemas/BarBar1",
                        "bar2": "#/components/schemas/BarBar2"
                      }
                    }
                  },
                  "right": {
                    "discriminator": {
                      "propertyName": "$type",
                      "mapping": {
                        "bar1": "#/components/schemas/BarBar1",
                        "bar2": "#/components/schemas/BarBar2"
                      }
                    }
                  }
                }
              }
            ],
            "discriminator": {
              "propertyName": "$type",
              "mapping": {
                "bar1": "#/components/schemas/BarBar1",
                "bar2": "#/components/schemas/BarBar2"
              }
            }
          }
        }
      },
      "FooFoo2": {
        "required": [
          "bar"
        ],
        "properties": {
          "$type": {
            "enum": [
              "foo2"
            ],
            "type": "string"
          },
          "musketeer": {
            "type": "integer",
            "format": "int32",
            "default": 3
          },
          "bar": {
            "required": [
              "$type"
            ],
            "type": "object",
            "anyOf": [
              {
                "properties": {
                  "$type": {
                    "enum": [
                      "bar1"
                    ],
                    "type": "string"
                  },
                  "hi": {
                    "type": "string",
                    "default": "Hi"
                  }
                }
              },
              {
                "required": [
                  "left",
                  "right"
                ],
                "properties": {
                  "$type": {
                    "enum": [
                      "bar2"
                    ],
                    "type": "string"
                  },
                  "left": {
                    "discriminator": {
                      "propertyName": "$type",
                      "mapping": {
                        "bar1": "#/components/schemas/BarBar1",
                        "bar2": "#/components/schemas/BarBar2"
                      }
                    }
                  },
                  "right": {
                    "discriminator": {
                      "propertyName": "$type",
                      "mapping": {
                        "bar1": "#/components/schemas/BarBar1",
                        "bar2": "#/components/schemas/BarBar2"
                      }
                    }
                  }
                }
              }
            ],
            "discriminator": {
              "propertyName": "$type",
              "mapping": {
                "bar1": "#/components/schemas/BarBar1",
                "bar2": "#/components/schemas/BarBar2"
              }
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Issue"
    }
  ]
}

This is essentially the same as #60339, but is different from #61717, #61407 which use arrays of types.

Expected Behavior

components["schemas"]["Bar"] should be generated

Adding a dummy endpoint to force the resolution of Bar (see the httpsFixed profile in the reproduction repository) fixes the issue.

{
  "openapi": "3.0.1",
  "info": {
    "title": "Issue | v1",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://localhost:7242/"
    }
  ],
  "paths": {
    "/foo": {
      "get": {
        "tags": [
          "Issue"
        ],
        "operationId": "GetFoo",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Foo"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Bar": {
        "required": [
          "$type"
        ],
        "type": "object",
        "anyOf": [
          {
            "$ref": "#/components/schemas/BarBar1"
          },
          {
            "$ref": "#/components/schemas/BarBar2"
          }
        ],
        "discriminator": {
          "propertyName": "$type",
          "mapping": {
            "bar1": "#/components/schemas/BarBar1",
            "bar2": "#/components/schemas/BarBar2"
          }
        }
      },
      "BarBar1": {
        "properties": {
          "$type": {
            "enum": [
              "bar1"
            ],
            "type": "string"
          },
          "hi": {
            "type": "string",
            "default": "Hi"
          }
        }
      },
      "BarBar2": {
        "required": [
          "left",
          "right"
        ],
        "properties": {
          "$type": {
            "enum": [
              "bar2"
            ],
            "type": "string"
          },
          "left": {
            "$ref": "#/components/schemas/Bar"
          },
          "right": {
            "discriminator": {
              "propertyName": "$type",
              "mapping": {
                "bar1": "#/components/schemas/BarBar1",
                "bar2": "#/components/schemas/BarBar2"
              }
            }
          }
        }
      },
      "Foo": {
        "required": [
          "$type"
        ],
        "type": "object",
        "anyOf": [
          {
            "$ref": "#/components/schemas/FooFoo1"
          },
          {
            "$ref": "#/components/schemas/FooFoo2"
          }
        ],
        "discriminator": {
          "propertyName": "$type",
          "mapping": {
            "foo1": "#/components/schemas/FooFoo1",
            "foo2": "#/components/schemas/FooFoo2"
          }
        }
      },
      "FooFoo1": {
        "required": [
          "bar"
        ],
        "properties": {
          "$type": {
            "enum": [
              "foo1"
            ],
            "type": "string"
          },
          "haha": {
            "type": "string",
            "default": "boo"
          },
          "bar": {
            "$ref": "#/components/schemas/Bar"
          }
        }
      },
      "FooFoo2": {
        "required": [
          "bar"
        ],
        "properties": {
          "$type": {
            "enum": [
              "foo2"
            ],
            "type": "string"
          },
          "musketeer": {
            "type": "integer",
            "format": "int32",
            "default": 3
          },
          "bar": {
            "$ref": "#/components/schemas/Bar"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Issue"
    }
  ]
}

Steps To Reproduce

See full repro at https://github.com/andrewimcclement/MinimalApiOpenApiGenerationIssue.

Exceptions (if any)

No response

.NET Version

9.0.203

Anything else?

ASP.NET version: 9.0.4
dotnet_info.txt

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcfeature-openapi

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions