From 5b6cd33fb1999a9fbec95584d3bc2628fdd2a7b8 Mon Sep 17 00:00:00 2001 From: jjh75607 Date: Fri, 28 Aug 2026 09:02:01 +0900 Subject: [PATCH] Replace nested Page with PagedModel when serialization mode is VIA_DTO Fixes #3319 --- .../core/converters/PageOpenAPIConverter.java | 26 ++++++--- .../api/v30/app10/HelloController.java | 10 ++++ .../api/v31/app10/HelloController.java | 10 ++++ .../resources/results/3.0.1/app10-direct.json | 56 +++++++++++++++++++ .../results/3.0.1/app10-via_dto.json | 56 +++++++++++++++++++ .../resources/results/3.1.0/app10-direct.json | 56 +++++++++++++++++++ .../results/3.1.0/app10-via_dto.json | 56 +++++++++++++++++++ 7 files changed, 262 insertions(+), 8 deletions(-) diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java index e80fe1944b..a761a1c93f 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/converters/PageOpenAPIConverter.java @@ -43,8 +43,6 @@ import org.springframework.core.ResolvableType; import org.springframework.data.web.PagedModel; -import static org.springdoc.core.utils.SpringDocUtils.getParentTypeName; - /** * The Spring Data Page type model converter. * @@ -119,7 +117,7 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato if (!type.isSchemaProperty()) type = resolvePagedModelType(javaType, type); else - type.name(getParentTypeName(type, cls)); + type.type(pagedModelType(javaType)).resolveAsRef(true); } } Schema schema = (chain.hasNext()) ? chain.next().resolve(type, context, chain) : null; @@ -137,11 +135,7 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato */ private AnnotatedType resolvePagedModelType(JavaType type, AnnotatedType originalType) { if (type.hasGenericTypes()) { - JavaType innerType = type.containedType(0); - Type pagedModelType = ResolvableType - .forClassWithGenerics(PagedModel.class, ResolvableType.forType(innerType)) - .getType(); - return new AnnotatedType(pagedModelType) + return new AnnotatedType(pagedModelType(type)) .resolveAsRef(true) .ctxAnnotations(originalType.getCtxAnnotations()); } @@ -150,6 +144,22 @@ private AnnotatedType resolvePagedModelType(JavaType type, AnnotatedType origina } } + /** + * The PagedModel type matching the given Page type. + * + * @param type the page type + * @return the paged model type + */ + private Type pagedModelType(JavaType type) { + if (type.hasGenericTypes()) { + JavaType innerType = type.containedType(0); + return ResolvableType + .forClassWithGenerics(PagedModel.class, ResolvableType.forType(innerType)) + .getType(); + } + return PagedModel.class; + } + /** * Sort page schema properties. * diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/HelloController.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/HelloController.java index 0a22c10535..e902b6b3e8 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/HelloController.java +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v30/app10/HelloController.java @@ -74,6 +74,16 @@ public PagedModel pagedModelRaw() { return pagedModelSimple(); } + @GetMapping("/dummy-page-simple") + public Dummy> dummyPageSimple() { + return new Dummy<>(pageSimple()); + } + + @GetMapping("/dummy-page-complex") + public Dummy>>> dummyPageComplex() { + return new Dummy<>(pageComplex()); + } + private PagedModel pagedModel(T value) { return new PagedModel<>(pageImpl(value)); } diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/HelloController.java b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/HelloController.java index 57327b9466..c7673f9755 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/HelloController.java +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/java/test/org/springdoc/api/v31/app10/HelloController.java @@ -68,6 +68,16 @@ public PagedModel pagedModelRaw() { return pagedModelSimple(); } + @GetMapping("/dummy-page-simple") + public Dummy> dummyPageSimple() { + return new Dummy<>(pageSimple()); + } + + @GetMapping("/dummy-page-complex") + public Dummy>>> dummyPageComplex() { + return new Dummy<>(pageComplex()); + } + private PagedModel pagedModel(T value) { return new PagedModel<>(pageImpl(value)); } diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json index d234570e66..12f41a67fc 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-direct.json @@ -150,6 +150,46 @@ } } } + }, + "/dummy-page-simple": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageSimple", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageString" + } + } + } + } + } + } + }, + "/dummy-page-complex": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageComplex", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageDummyListString" + } + } + } + } + } + } } }, "components": { @@ -462,6 +502,22 @@ "type": "boolean" } } + }, + "DummyPageString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "DummyPageDummyListString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PageDummyListString" + } + } } } } diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json index 6b4da91923..ed88f14ce1 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.0.1/app10-via_dto.json @@ -150,6 +150,46 @@ } } } + }, + "/dummy-page-simple": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageSimple", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageString" + } + } + } + } + } + } + }, + "/dummy-page-complex": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageComplex", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageDummyListString" + } + } + } + } + } + } } }, "components": { @@ -252,6 +292,22 @@ "type": "string" } } + }, + "DummyPageString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PagedModelString" + } + } + }, + "DummyPageDummyListString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PagedModelDummyListString" + } + } } } } diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json index 1c93f179f7..e3fa6c9958 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-direct.json @@ -130,6 +130,46 @@ } } } + }, + "/dummy-page-simple": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageSimple", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageString" + } + } + } + } + } + } + }, + "/dummy-page-complex": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageComplex", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageDummyListString" + } + } + } + } + } + } } }, "components": { @@ -381,6 +421,22 @@ "type": "boolean" } } + }, + "DummyPageString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PageString" + } + } + }, + "DummyPageDummyListString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PageDummyListString" + } + } } } } diff --git a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json index f7853b31f3..d8d18c9cfc 100644 --- a/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json +++ b/springdoc-openapi-tests/springdoc-openapi-hateoas-tests/src/test/resources/results/3.1.0/app10-via_dto.json @@ -130,6 +130,46 @@ } } } + }, + "/dummy-page-simple": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageSimple", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageString" + } + } + } + } + } + } + }, + "/dummy-page-complex": { + "get": { + "tags": [ + "hello-controller" + ], + "operationId": "dummyPageComplex", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DummyPageDummyListString" + } + } + } + } + } + } } }, "components": { @@ -205,6 +245,22 @@ "$ref": "#/components/schemas/PageMetadata" } } + }, + "DummyPageString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PagedModelString" + } + } + }, + "DummyPageDummyListString": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/PagedModelDummyListString" + } + } } } }