Skip to content

Page Type Information Lost When Wrapped in Generic Response Class - Page<T> Becomes PageObject #3192

@wanghongzhou

Description

@wanghongzhou

Describe the bug

When a Page<T> type is wrapped inside a generic response wrapper (like ApiResponse<Page<UserVO>>), SpringDoc loses the generic type information and generates an incorrect schema where Page<UserVO> becomes PageObject instead of maintaining the proper generic type. This happens even with VIA_DTO serialization mode enabled.

To Reproduce

Versions:

  • Spring Boot: 3.5.9
  • SpringDoc OpenAPI: 2.8.15
  • Spring Data: 2025.0.0

Configuration:

spring:
  data:
    web:
      pageable:
        serialization-mode: VIA_DTO

Response wrapper:

@Data
@Schema(description = "Unified JSON Response")
public class ApiResponse<D> {
    @Schema(description = "Status code")
    private int code;
    
    @Schema(description = "Response data")
    private D data;  // When D is Page<UserVO>, type info is lost
}

Controller:

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping
    @Operation(summary = "Get paginated users")
    public ApiResponse<Page<UserVO>> getUsers(Pageable pageable) {
        // Implementation
    }
}

@Data
@Schema(description = "User View Object")
class UserVO {
    @Schema(description = "Username")
    private String username;
    
    @Schema(description = "Email")
    private String email;
}

Actual OpenAPI Schema Generated:

{
  "ApiResponsePageUserVO": {
    "type": "object",
    "properties": {
      "code": { "type": "integer" },
      "data": {
        "$ref": "#/components/schemas/PageObject"  // ❌ Wrong! Should be Page«UserVO»
      }
    }
  },
  "PageObject": {  // ❌ Generic type lost
    "type": "object",
    "properties": {
      "content": {
        "type": "array",
        "items": {}  // ❌ Empty items - no UserVO type
      }
    }
  }
}

Expected OpenAPI Schema:

{
  "ApiResponsePageUserVO": {
    "type": "object",
    "properties": {
      "code": { "type": "integer" },
      "data": {
        "$ref": "#/components/schemas/PageUserVO"  // ✅ Correct reference
      }
    }
  },
  "PageUserVO": {  // ✅ Proper generic type preserved
    "type": "object",
    "properties": {
      "content": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/UserVO"  // ✅ Correct item type
        }
      },
      "pageable": { "$ref": "#/components/schemas/PageableObject" },
      "totalElements": { "type": "integer" },
      "totalPages": { "type": "integer" }
      // ... other VIA_DTO properties
    }
  },
  "UserVO": {
    "type": "object",
    "properties": {
      "username": { "type": "string" },
      "email": { "type": "string" }
    }
  }
}

Root Cause Analysis

In PageOpenAPIConverter.resolve():

if (replacePageWithPagedModel && PAGE_TO_REPLACE.equals(cls.getCanonicalName())) {
    if (!type.isSchemaProperty())
        type = resolvePagedModelType(javaType);  // ✅ Preserves generic type
    else
        type.name(getParentTypeName(type, cls)); // ❌ Only sets name, loses generic type
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions