DEBUG.WIKI
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →
RackNerd;$21.99/年 · 1GB KVM

Kotlinx Serialization 返回 PageImpl 报找不到 serializer,改成明确的分页响应

返回 Page 时 Kotlinx Serialization 报找不到 PageImpl serializer,使用明确的分页 DTO,保留元素类型、页号与总数契约。

Controller 返回的是 Page<T>,查询已经完成,写 HTTP 响应时却变成 500:

HttpMessageNotWritableException
Serializer for subclass 'PageImpl' is not found in the polymorphic scope of 'Page'

先确认实际选中的消息转换器。如果请求走的是 Kotlinx Serialization,就不能因为另一个 Jackson 接口能输出分页结果,认定 PageImpl 在这里也有可用的 serializer。Kotlinx Serialization 采用显式序列化模型,Spring Data 的分页实现没有因此自动变成可序列化类型。

2026 年 9 月的这类故障发生在 Spring Boot Web 应用中。关键条件是转换器与返回类型的组合,不能把所有分页 500 都归到某个 Boot 补丁版本。

把分页容器转换为应用自己的类型

如果接口由项目控制,可以只输出需要的内容和分页元数据。下面示例要求工程已启用 Kotlin serialization 编译插件,且 kotlinx-serialization-json 及对应 Spring 消息转换器已正常配置:

import kotlinx.serialization.Serializable
import org.springframework.data.domain.Page

@Serializable
data class ItemView(val id: Long, val name: String)

@Serializable
data class ItemPage(
    val items: List<ItemView>,
    val page: Int,
    val size: Int,
    val totalElements: Long,
    val totalPages: Int
)

fun Page<ItemView>.toResponse() = ItemPage(
    items = content,
    page = number,
    size = size,
    totalElements = totalElements,
    totalPages = totalPages
)

ItemView 是应用的响应模型,不直接放入 JPA 实体或任意第三方对象。Controller 的声明返回类型也改为 ItemPage,并返回查询结果的 toResponse();只转换内部数据、外层仍返回 PageImpl,还会保留原来的序列化边界。

示例里的 page 沿用 Spring Data 从零开始的页号。已有接口如果使用从一开始的页码或不同字段名,应明确转换,不能悄悄改变客户端约定。记录总数使用 Long,也不要为了前端方便强转成 Int。

若还需要排序、是否末页或其他信息,应按接口需要补到响应类型,而不是把 PageImpl 的所有实现细节原样复制进新 DTO。

为什么给业务实体加 Serializable 还不够

异常指出的是外层 PageImpl,不是列表里某一个元素。给元素加上 @Serializable,只能解决元素自身的生成式序列化,不能自动为第三方分页实现注册多态子类。

同样,给泛型接口做一个空的 SerializersModule,或打开忽略未知字段,都不会生成缺失的分页 serializer。后者处理的是读取 JSON 时多出来的字段,与当前输出响应失败无关。

已有接口必须保留 Page 形状时,可以实现并注册完整的自定义 serializer;需要处理内容元素的 serializer、泛型信息和原 JSON 契约。这比明确的响应 DTO 更复杂,不能只加一条多态注册就假定所有 Page<T> 都恢复。

验证真正经过 HTTP 的返回结果

保留原请求的 Accept、Controller 声明类型和转换器顺序,检查空页、非空页、末页与大总数。应看到响应成功、字段名和页号规则正确、每个元素都能编码。

不要把测试改成直接调用 Jackson 的 mapper 来证明这个接口已修好;那会绕开原来失败的转换器。若项目决定改回 Jackson,应作为另一项明确的序列化配置变更,连同其他接口一起回归,而不是同时留下两套互相竞争的默认设置。