Issue Reference: springdoc/springdoc-openapi#3161
Bug: HAL_linksfield is duplicated in subtypes extending RepresentationModel
- Overview
- Bug Description
- Test Results
- Quick Start
- Testing Different Versions
- Reproduction Steps
- Project Structure
- Key Code
- Debugging Tips
- Contributing
This is a minimal Spring Boot application to reproduce issue #3161 in the Springdoc OpenAPI library.
Core Problem: When a subtype (ExtendedTestDto) extends a DTO (TestDto) that itself extends Spring HATEOAS's RepresentationModel, the _links field is incorrectly duplicated in the OpenAPI schema.
"ExtendedTestDto": {
"allOf": [
{ "$ref": "#/components/schemas/TestDto" },
{
"type": "object",
"properties": {
"otherField": { "type": "string" }
// _links should NOT appear here (inherited from TestDto)
}
}
]
}"ExtendedTestDto": {
"allOf": [
{ "$ref": "#/components/schemas/TestDto" },
{
"type": "object",
"properties": {
"otherField": { "type": "string" },
"_links": { // β Duplicate!
"$ref": "#/components/schemas/Links"
}
}
}
]
}Issue: The _links field appears in both TestDto (parent) and ExtendedTestDto (child), causing duplication.
Tested across multiple version combinations:
| Spring Boot | springdoc-openapi | Spring Framework | Result | Notes |
|---|---|---|---|---|
| 3.4.1 | 2.7.0 | 6.2.x | β Works | Recommended baseline |
| 3.5.9 | 2.8.14 | 6.1.x | β Bug | Originally reported version |
| 3.5.9 | 2.8.15 | 6.1.x | β Bug | Bug persists in latest 2.x |
| 4.0.1 | 3.0.1 | 6.2.x | β Bug | Bug exists in Spring Boot 4.x |
- β springdoc-openapi 2.7.0 works correctly (no bug)
- β springdoc-openapi 2.8.14+ exhibits the bug
- β Spring Boot 4.0.1 + springdoc-openapi 3.0.1 still has the bug
- π Likely regression between 2.7.0 β 2.8.14
- Java 17+
- Gradle 8.x (included via wrapper)
- Git (optional)
# 1. Clone repository (or download ZIP)
git clone https://github.com/YOUR_USERNAME/springdoc-issue-3161-reproduction.git
cd springdoc-issue-3161-reproduction
# 2. Run tests with default configuration (3.5.9 + 2.8.14 - bug version)
./gradlew clean test --tests SimpleBugReproductionTest
# 3. Start application (optional)
./gradlew bootRun
# 4. Check in browser
# - Swagger UI: http://localhost:8080/swagger-ui.html
# - OpenAPI JSON: http://localhost:8080/v3/api-docsThis project uses a single branch approach. Modify build.gradle to test different version combinations.
Edit build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1' // β Change here
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' // β Change here
// ... rest remains same
}Run test:
./gradlew clean test --tests SimpleBugReproductionTestExpected output:
====================================
π Testing with MockMvc
====================================
Spring Boot Version: 3.4.1
Springdoc OpenAPI Version: 2.7.0
β
Bug NOT reproduced
ExtendedTestDto schema is correct
build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.9' // Default
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14' // Default
// ... rest remains same
}Run test:
./gradlew clean test --tests SimpleBugReproductionTestExpected output:
====================================
π Testing with MockMvc
====================================
Spring Boot Version: 3.5.9
Springdoc OpenAPI Version: 2.8.14
β Bug reproduced!
ExtendedTestDto has both:
1. allOf: [TestDto] (inheritance)
2. properties._links (duplicate)
Expected: _links should only be in TestDto (via RepresentationModel)
Actual: _links appears in both TestDto AND ExtendedTestDto
build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.9'
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.15' // β Latest 2.x
// ... rest remains same
}Run test:
./gradlew clean test --tests SimpleBugReproductionTestExpected output: β Bug reproduced (bug persists in 2.8.15)
build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '4.0.1' // β Spring Boot 4.x
id 'io.spring.dependency-management' version '1.1.7'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' // β 3.x version
// ... rest remains same
}Run test:
./gradlew clean test --tests SimpleBugReproductionTestExpected output: β Bug reproduced (bug exists in Spring Boot 4.x)
# 1. Run test
./gradlew clean test --tests SimpleBugReproductionTest
# 2. Run with detailed logs
./gradlew clean test --tests SimpleBugReproductionTest --info
# 3. Run all tests
./gradlew clean test# 1. Start application
./gradlew bootRun
# 2. Check OpenAPI spec in terminal
curl http://localhost:8080/v3/api-docs | jq '.components.schemas.ExtendedTestDto'
# 3. Check Swagger UI in browser
open http://localhost:8080/swagger-ui.htmlCheck the ExtendedTestDto schema in OpenAPI JSON (/v3/api-docs):
β Correct (No Bug):
"ExtendedTestDto": {
"allOf": [
{ "$ref": "#/components/schemas/TestDto" },
{
"type": "object",
"properties": {
"otherField": { "type": "string" }
// No _links field here!
}
}
]
}β Bug (Duplicate _links):
"ExtendedTestDto": {
"allOf": [
{ "$ref": "#/components/schemas/TestDto" },
{
"type": "object",
"properties": {
"otherField": { "type": "string" },
"_links": { // Duplicate!
"$ref": "#/components/schemas/Links"
}
}
}
]
}springdoc-issue-3161-reproduction/
βββ README.md # This file
βββ build.gradle # Gradle build configuration
βββ settings.gradle # Project settings
βββ .gitignore # Git ignore file
βββ gradlew # Gradle Wrapper (Unix)
βββ gradlew.bat # Gradle Wrapper (Windows)
βββ gradle/
β βββ wrapper/
β βββ gradle-wrapper.jar # Gradle Wrapper JAR
β βββ gradle-wrapper.properties # Gradle version config
βββ src/
βββ main/
β βββ java/io/github/huisoo/reproduction/springdocissue3161/
β β βββ SpringdocIssue3161ReproductionApplication.java # Main class
β β βββ controller/
β β β βββ TestController.java # /hello endpoint
β β βββ dto/
β β βββ TestDto.java # Parent DTO
β β βββ ExtendedTestDto.java # Child DTO (bug demo)
β βββ resources/
β βββ application.properties # Spring config
βββ test/
βββ java/io/github/huisoo/reproduction/springdocissue3161/
βββ SimpleBugReproductionTest.java # Reproduction test
@Data
@EqualsAndHashCode(callSuper = false)
@Schema(
description = "Base test DTO extending RepresentationModel",
subTypes = { ExtendedTestDto.class }
)
public class TestDto extends RepresentationModel<TestDto> {
@Schema(description = "ID field")
private Long id;
@Schema(description = "Name field")
private String name;
// _links is automatically provided by RepresentationModel
}@Data
@EqualsAndHashCode(callSuper = true)
@Schema(
description = "Extended test DTO",
allOf = { TestDto.class } // β Inherits via allOf
)
public class ExtendedTestDto extends TestDto {
@Schema(description = "Additional field specific to ExtendedTestDto")
private String otherField;
// _links should be inherited from TestDto (parent)
// Bug: _links incorrectly appears here as duplicate!
}@RestController
@Tag(name = "Test", description = "Test API for reproducing springdoc-openapi issue #3161")
public class TestController {
@GetMapping("/hello")
@Operation(
summary = "Get ExtendedTestDto",
description = "Returns an ExtendedTestDto to demonstrate _links duplication bug"
)
public ExtendedTestDto hello() {
ExtendedTestDto dto = new ExtendedTestDto();
dto.setId(1L);
dto.setName("Test");
dto.setOtherField("Additional Data");
// Add HATEOAS link
Link selfLink = linkTo(methodOn(TestController.class).hello()).withSelfRel();
dto.add(selfLink);
return dto;
}
}@SpringBootTest(classes = SpringdocIssue3161ReproductionApplication.class)
@AutoConfigureMockMvc
public class SimpleBugReproductionTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void reproduceBugWithMockMvc() throws Exception {
// Get OpenAPI spec
MvcResult result = mockMvc.perform(get("/v3/api-docs"))
.andExpect(status().isOk())
.andReturn();
String openApiJson = result.getResponse().getContentAsString();
JsonNode rootNode = objectMapper.readTree(openApiJson);
// Check ExtendedTestDto schema
JsonNode extendedTestDto = rootNode
.path("components")
.path("schemas")
.path("ExtendedTestDto");
// Bug check: ExtendedTestDto should NOT have duplicate _links
boolean hasAllOf = extendedTestDto.has("allOf");
boolean hasOwnLinks = extendedTestDto.path("properties").has("_links");
// Assertion: _links should NOT exist (inherited from TestDto)
assertThat(hasOwnLinks)
.as("ExtendedTestDto should NOT have own _links property")
.isFalse();
}
}# View dependency tree
./gradlew dependencies --configuration runtimeClasspath | grep -E "spring-boot|springdoc"
# Check specific dependency version
./gradlew dependencyInsight --dependency springdoc-openapi-starter-webmvc-ui
./gradlew dependencyInsight --dependency spring-boot-starter-web# Save working version (3.4.1 + 2.7.0) spec
curl http://localhost:8080/v3/api-docs | jq . > openapi-working.json
# Modify build.gradle to bug version...
# Save bug version (3.5.9 + 2.8.14) spec
curl http://localhost:8080/v3/api-docs | jq . > openapi-bug.json
# Compare differences
diff -u openapi-working.json openapi-bug.jsoncurl -s http://localhost:8080/v3/api-docs | \
jq '.components.schemas.ExtendedTestDto'src/main/resources/application.properties:
# Enable detailed logging
logging.level.org.springdoc=DEBUG
logging.level.org.springframework.hateoas=DEBUG
logging.level.io.swagger=DEBUG| Endpoint | Description |
|---|---|
GET /hello |
Returns ExtendedTestDto for testing |
GET /v3/api-docs |
OpenAPI 3.0 specification (JSON) |
GET /swagger-ui.html |
Swagger UI |
| Spring Boot | Spring Framework | Notes |
|---|---|---|
| 3.4.x | 6.2.x | Supports LiteWebJarsResourceResolver |
| 3.5.x | 6.1.x | Does not support LiteWebJarsResourceResolver |
| 4.0.x | 6.2.x | Spring Boot 4.x (latest) |
| springdoc-openapi | Recommended Spring Boot | Spring Framework | Issue #3161 Status |
|---|---|---|---|
| 2.7.0 | 3.4.x | 6.2.x | β Works |
| 2.8.14 | 3.5.x | 6.1.x | β Bug |
| 2.8.15 | 3.5.x | 6.1.x | β Bug |
| 3.0.1 | 4.0.x | 6.2.x | β Bug |
Contributions to improve this reproduction or test additional versions are welcome!
- Fork this repository
- Create a feature branch (
git checkout -b feature/test-new-version) - Commit your changes (
git commit -m 'Add test for version X.Y.Z') - Push to the branch (
git push origin feature/test-new-version) - Open a Pull Request
If you test a new version combination, please include:
- Spring Boot version
- springdoc-openapi version
- Spring Framework version (auto-determined)
- Bug reproduction status (β Works / β Bug)
- Test execution logs (optional)
This project is licensed under the MIT License.
- Original Issue: springdoc/springdoc-openapi#3161
- Springdoc OpenAPI Docs: https://springdoc.org/
- Spring HATEOAS Docs: https://docs.spring.io/spring-hateoas/docs/current/reference/html/
- OpenAPI 3.0 Spec: https://swagger.io/specification/
- Maintainer: @huisoo
- GitHub: https://github.com/huisoo/springdoc-issue-3161-reproduction
- Issue Tracker: GitHub Issues
This reproduction project is created with appreciation for the excellent work of the springdoc-openapi team, and aims to contribute to resolving this issue.
Last Updated: 2026-01-10