Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stack Overflow / infinite recursion in PolymorphicModelConverter.findComposedSchemas #2801

Closed
martinitus opened this issue Dec 2, 2024 · 6 comments · Fixed by #2833
Closed
Labels
incomplete incomplete description: Make sure you Provide a Minimal, Reproducible Example - with HelloController

Comments

@martinitus
Copy link
Contributor

Describe the bug
I am trying to work around the problem described here with the workaround described here. TLDR: I want to have a description on a type that is not defined by the type, but by the context where the type is used.

To Reproduce

  • spring-boot 3.3
  • springdoc-openapi 2.70
// sorry for the Kotlin code, my Java is a little outdated...
import io.swagger.v3.oas.annotations.media.Schema
import kotlinx.serialization.Serializable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController


@Serializable
@Schema(description = "Generic description")
data class KeyValue(
    val key: String,
    val value: String,
)

@Serializable
@Schema
data class SomeDTO(
    @Schema(description = "Description A", allOf = [KeyValue::class]) val field_a: KeyValue,
    @Schema(description = "Description B", allOf = [KeyValue::class]) val field_b: KeyValue,
)

@RestController
@RequestMapping("/test")
class TestAdapter() {
    
    @PostMapping("/test")
    fun create(@RequestBody some: SomeDTO) {
        TODO()
    }
}

Expected behavior
Expected Open API Json

... component/schemas/
"SomeDTO": {
        "required": [
          "field_a",
          "field_b"
        ],
        "type": "object",
        "properties": {
          "field_a": {
            "type": "object",
            "description": "Description A",
            "allOf": [
              {
                "$ref": "#/components/schemas/KeyValue"
              }
            ]
          },
          "field_b": {
            "type": "object",
            "description": "Description B",
            "allOf": [
              {
                "$ref": "#/components/schemas/KeyValue"
              }
            ]
          }
        }
      },

Screenshots

It loads (no StackOverflow) and renders correctly, when I change type of field_a and field_b to String. But that's obviously not what I want.

@Schema(description = "Description A", allOf = [KeyValue::class]) val field_a: String,
@Schema(description = "Description B", allOf = [KeyValue::class]) val field_b: String,

image

** Stack Trace**

11:10:45.221 [http-nio-8080-exec-1] /v3/api-docs INFO  d.b...RequestLoggingFilter - method=GET
11:10:45.806 [http-nio-8080-exec-1]  ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed: java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
	at org.springdoc.core.converters.PolymorphicModelConverter.lambda$findComposedSchemas$2(PolymorphicModelConverter.java:147)
	at java.base/java.util.stream.MatchOps$1MatchSink.accept(MatchOps.java:90)
	at java.base/java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1602)
	at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:129)
	at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:527)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:513)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:230)
	at java.base/java.util.stream.MatchOps$MatchOp.evaluateSequential(MatchOps.java:196)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:632)
	at org.springdoc.core.converters.PolymorphicModelConverter.lambda$findComposedSchemas$3(PolymorphicModelConverter.java:147)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:178)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
	at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:179)
	at java.base/java.util.TreeMap$ValueSpliterator.forEachRemaining(TreeMap.java:3215)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)
	at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
	at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
	at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:622)
	at java.base/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:627)
	at org.springdoc.core.converters.PolymorphicModelConverter.findComposedSchemas(PolymorphicModelConverter.java:149)
@bnasslahsen
Copy link
Contributor

@martinitus,

Not reproducible.
Feel free to provide a Minimal, Reproducible Example - with HelloController that reproduces the problem.

This ticket will be closed, but can be reopened if your provide the reproducible sample.

@bnasslahsen bnasslahsen added the incomplete incomplete description: Make sure you Provide a Minimal, Reproducible Example - with HelloController label Dec 7, 2024
@martinitus
Copy link
Contributor Author

martinitus commented Dec 7, 2024

@bnasslahsen

Here you go: https://github.com/martinitus/springdoc-openapi-sample

Run app and open localhost:8080/swagger-ui.html. 💥

I've seen you have kotlin tests in here, and I tried to get them going on my non-work windows pc for like 15minutes, but then gave up. Should be easy to add one test scenario that includes this :-)

@bnasslahsen bnasslahsen reopened this Dec 7, 2024
@martinitus
Copy link
Contributor Author

martinitus commented Jan 2, 2025

@bnasslahsen I think when migrating to OpenAPI v3.1 the workaround I am trying to achieve would no longer be necessary: I was able to get the desired behaviour out of the swagger-core library.

I'll keep experimenting and see if I can get this working with the current springdoc-openapi version.

I also have a small branch that would resolve the infinite recursion, but I'm not sure whether my changes make sense.. I'll add a link to the fork later.

@omarfi
Copy link

omarfi commented Jan 2, 2025

I am experiencing the same problem after upgrading spring boot to 3.4.1 and springdoc to 2.7.0. I have two self-referential models (ModelA contains a field of type ModelA), and this type of modelling is no longer supported in 2.7.0 it seems. It keeps on recursing in org.springdoc.core.converters.PolymorphicModelConverter#findComposedSchemas.

If I remove these models from my code the error is gone. Also, downgrading to v2.5.0 removes the error.

I have a wild hunch that this commit is the culprit: 497bfae

@martinitus
Copy link
Contributor Author

It might make sense to add the self referential model scenario as a unit test, as it is unrelated to the solution i have in #2833.

@martinitus
Copy link
Contributor Author

@omarfi I could not reproduce your issue with a simple self-referential model in the latest main commit after #2833 was merged:

@Schema
data class TreeNode(
	@Schema(description = "The children") val children: List<TreeNode>,
	@Schema(description = "Root nodes don't have a parent") val parent: TreeNode?,
)

@RestController
@RequestMapping("/test")
class TestController {

	@PostMapping("/test")
	fun create(@RequestBody root: TreeNode) {

	}
}

So I guess its resolved :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
incomplete incomplete description: Make sure you Provide a Minimal, Reproducible Example - with HelloController
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants