PowerSync对接Supabase时CRUD上传序列化错误求助
通过PowerSync将移动端应用对接Supabase,已完成登录且曾成功上传过数据,但目前客户端创建/更新数据时无法同步至Supabase,报错:
Error uploading crud: Serializer for subclass 'SerializedRow' is not found in the polymorphic scope of 'SqliteRow'.
Check if class with serial name 'SerializedRow' exists and serializer is registered in a corresponding SerializersModule.
To be registered automatically, class 'SerializedRow' has to be '@Serializable', and the base class 'SqliteRow' has to be sealed and '@Serializable'.
推测是序列化问题,尝试将id、userId、date从UUID/LocalDate改为String未解决,相关代码如下:
序列化对象代码
object UUIDSerializer : KSerializer<UUID> { override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): UUID { return UUID.fromString(decoder.decodeString()) } override fun serialize(encoder: Encoder, value: UUID) { encoder.encodeString(value.toString()) } } object LocalDateSerializer : KSerializer<LocalDate> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("_root_ide_package_.java.time.LocalDate", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: LocalDate) { encoder.encodeString(value.toString()) } override fun deserialize(decoder: Decoder): LocalDate = LocalDate.parse(decoder.decodeString()) }
Task实体类代码
@Serializable data class Task ( // @Serializable(with = UUIDSerializer::class) // val id: UUID, val id: String, // 2026-04-21 08:15:34.082732+00 - this is always stored at UTC time zone @SerialName("created_at") val createdAt: String? = null, @SerialName("user_id") // @Serializable(with = UUIDSerializer::class) // val userId: UUID, val userId: String, // @Serializable(with = LocalDateSerializer::class) // val date: LocalDate? = null, // 2026-04-22 val date: String? = null, @SerialName("start_time") // 10:14:00+02 - this is stored with the local time zone var startTime: String? = null, @SerialName("end_time") // 10:58:00+02 var endTime: String? = null, var title: String, var description: String? = null, var priority: Int? = null, val pinned: Boolean = false )
TaskDAO代码
@Singleton class TaskDAO @Inject constructor( private val dataSource: OnlineSyncDataSource ) { suspend fun insertTask(task: Task) { try { dataSource.getDatabase().writeTransaction { transaction -> transaction.execute( sql = "INSERT INTO tasks (id, created_at, user_id, date, start_time, end_time, title, description, priority, pinned) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", parameters = listOf( task.id, task.createdAt, task.userId, task.date, task.startTime, task.endTime, task.title, task.description, task.priority, task.pinned ) ) } } catch (e: Exception) { Log.e("TaskDAO", e.printStackTrace().toString()) } } suspend fun updateTask(task: Task) { try { dataSource.getDatabase().execute( sql = "UPDATE tasks SET date = ?, start_time = ?, end_time = ?, title = ?, description = ?, priority = ?, pinned = ? WHERE id = ?", parameters = listOf( task.date, task.startTime, task.endTime, task.title, task.description, task.priority, task.pinned, task.id ) ) } catch (e: Exception) { Log.e("TaskDAO", e.printStackTrace().toString()) } } }
TasksRepository代码
@Singleton class TasksRepository @Inject constructor( private val dao: TaskDAO ) { suspend fun insertTask(task: Task): Unit { return dao.insertTask(task) } suspend fun updateTask(task: Task): Unit { return dao.updateTask(task) } }
PowerSync数据库Schema代码
val AppSchema: Schema = Schema( listOf( Table( name = "tasks", columns = listOf( Column.text("created_at"), Column.text("user_id"), Column.text("date"), Column.text("start_time"), Column.text("end_time"), Column.text("title"), Column.text("description"), Column.text("priority"), Column.text("pinned") ) ) ) )
OnlineSyncDataSource代码
@OptIn(DelicateCoroutinesApi::class) @Singleton class OnlineSyncDataSource @Inject constructor( private val powerSyncDatabase: PowerSyncDatabase, private val supabaseClient: SupabaseClient ) { val dotenv = dotenv() init { GlobalScope.launch { powerSyncDatabase.connect(SupabaseConnector( supabaseClient, dotenv["POWERSYNC_URL"] )) } } fun getDatabase(): PowerSyncDatabase { return powerSyncDatabase } }
这个错误的核心是PowerSync同步时的序列化配置缺失,加上Schema与数据类型不匹配导致的,按以下步骤修正:
1. 注册多态序列化模块
PowerSync依赖Kotlinx Serialization处理数据同步,需显式注册SerializedRow到多态序列化模块。在应用初始化代码(如Application类)中添加:
import androidx.powersync.sqlite.SerializedRow import androidx.powersync.sqlite.SqliteRow import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass import kotlinx.serialization.json.Json // 全局序列化模块 val appSerializersModule = SerializersModule { polymorphic(SqliteRow::class) { subclass(SerializedRow::class) } } // 配置Json序列化器 val json = Json { serializersModule = appSerializersModule ignoreUnknownKeys = true }
创建PowerSyncDatabase时传入该序列化器:
val powerSyncDatabase = PowerSyncDatabase.Builder(context, AppSchema) .serializer(json) .build()
2. 修正PowerSync Schema的类型与结构
你的Schema存在两个关键错误:
- 缺失主键
id列,PowerSync需要主键跟踪数据变更 priority、pinned字段类型与实体类不匹配
修改后的Schema:
val AppSchema: Schema = Schema( listOf( Table( name = "tasks", columns = listOf( Column.text("id"), Column.text("created_at").nullable(), Column.text("user_id"), Column.text("date").nullable(), Column.text("start_time").nullable(), Column.text("end_time").nullable(), Column.text("title"), Column.text("description").nullable(), Column.integer("priority").nullable(), // 匹配实体类Int?类型 Column.boolean("pinned") // 匹配实体类Boolean类型 ), primaryKey = listOf("id") // 明确主键 ) ) )
3. 修正DAO中的参数空值处理
避免传入null导致SQL执行异常,在DAO的参数列表中显式处理空值:
// insertTask的parameters修改示例 parameters = listOf( task.id, task.createdAt, task.userId, task.date, task.startTime, task.endTime, task.title, task.description, task.priority, task.pinned )
如果数据库不允许某些字段为null,可设置默认值(如task.description ?: ""),同时对应Schema去掉.nullable()。
4. 优化PowerSync初始化逻辑
避免使用GlobalScope,改用应用级别CoroutineScope:
@OptIn(DelicateCoroutinesApi::class) @Singleton class OnlineSyncDataSource @Inject constructor( private val powerSyncDatabase: PowerSyncDatabase, private val supabaseClient: SupabaseClient, private val applicationScope: CoroutineScope ) { val dotenv = dotenv() init { applicationScope.launch { try { powerSyncDatabase.connect(SupabaseConnector( supabaseClient, dotenv["POWERSYNC_URL"] )) } catch (e: Exception) { Log.e("PowerSync", "连接失败", e) } } } fun getDatabase(): PowerSyncDatabase { return powerSyncDatabase } }
5. 重置数据库(可选)
若之前的Schema错误导致本地数据库结构异常,可卸载应用重新安装,或在开发环境调用powerSyncDatabase.reset()重置数据库,确保新Schema生效。
内容的提问来源于stack exchange,提问作者Joyce Opio

