Summary
During a routine upgrade to Spring Boot 3.5.11 and Springdoc 2.8.15, the application encountered a critical runtime failure when attempting to access the generated OpenAPI documentation. While the application started successfully, any request to the Swagger UI or the /v3/api-docs endpoint resulted in a 500 Internal Server Error. The error manifested as a java.lang.NoSuchMethodError, specifically targeting the Schema.requiredMode() method within the Swagger annotations library.
Root Cause
The root cause is a transitive dependency mismatch, commonly known as “Dependency Hell.”
- The application uses
@Parameterand@OpenApiDefinitionannotations which belong to the swagger-annotations library. - The
java.lang.NoSuchMethodErrorindicates that at runtime, the JVM loaded a version of theio.swagger.v3.oas.annotations.media.Schemaclass that does not contain therequiredMode()method. - This happens because
springdoc-openapidepends on a specific version ofswagger-core, but another dependency in the classpath (often brought in by Spring Boot’s dependency management or other starters) is forcing an older, incompatible version of the swagger annotations onto the classpath. - The method
requiredMode()was introduced in later versions of the Swagger/OpenAPI specification; older versions only used a booleanrequiredfield, causing the method lookup to fail.
Why This Happens in Real Systems
In complex microservices, dependency graphs are rarely flat. This issue occurs due to:
- Dependency Management Overrides: Spring Boot’s BOM (Bill of Materials) manages versions for hundreds of libraries. If a specific version of a library is managed by the Spring Boot parent, it may downgrade a library required by a third-party plugin like
springdoc. - Transitive Shadowing: Multiple libraries may depend on different versions of the same low-level utility (e.g.,
jackson,swagger-annotations, orslf4j). The build tool (Gradle/Maven) uses a conflict resolution strategy (usually “newest wins” or “nearest wins”) that might select a version that is binary incompatible with one of your explicit declarations. - Classpath Pollution: In large monorepos, shared internal libraries might bundle specific versions of annotations, leading to unexpected version shadowing when integrated into a new service.
Real-World Impact
- Deployment Blockers: The service appears “healthy” to basic Liveness/Readiness probes (which usually check
/actuator/health), but fails as soon as an automated tool or developer attempts to consume the API documentation. - Integration Failures: Automated client generators (like OpenAPI Generator) will fail to run against the service, breaking the Contract-First development workflow.
- Production Risk: If documentation is used for automated monitoring or gateway configuration (e.g., Spring Cloud Gateway), the failure can lead to routing errors or visibility gaps in production environments.
Example or Code (if necessary and relevant)
To diagnose this in Gradle, you must inspect the dependency tree to see which library is pulling in the older version:
./gradlew dependencyInsight --configuration compileClasspath --dependency swagger-annotations
To fix it, you must explicitly force the correct version in your build.gradle:
dependencies {
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15")
// Force the version to ensure binary compatibility with springdoc 2.x
implementation("io.swagger.core.v3:swagger-annotations:2.2.22")
}
How Senior Engineers Fix It
A senior engineer does not simply “add the dependency” and move on. The professional approach involves:
- Dependency Analysis: Using
gradle dependenciesormvn dependency:treeto locate the exact origin of the conflict. - Strict Version Alignment: Utilizing Dependency Constraints or a BOM to ensure that all modules in the project agree on a single, compatible version of the library.
- Verification: Running a test case that specifically triggers the OpenAPI generation logic during the CI/CD pipeline to catch
NoSuchMethodErrorbefore it reaches production. - Exclusion: If a specific library is bringing in a “poisoned” transitive dependency, use the
excludekeyword to remove it entirely from that specific path.
Why Juniors Miss It
- Surface-Level Debugging: Juniors often look at the code logic (the
@Parameterannotation) and assume the code is wrong, rather than looking at the compiled bytecode and the classpath. - Misinterpreting Errors: A
NoSuchMethodErroris frequently mistaken for a logic error or a missing implementation, when it is actually a versioning/linkage error. - Lack of Tooling Knowledge: Juniors often attempt to fix version issues by manually adding
implementationlines one by one without understanding the underlying dependency graph, which often leads to further conflicts later.