feat(typegen): add Kotlin type generation#1086
Conversation
Adds a Kotlin generator to gen types, alongside the existing TypeScript, Go, Swift, and Python targets. - New template at src/server/templates/kotlin.ts emitting @serializable data classes (kotlinx.serialization), one per table/view per operation (Select/Insert/Update), plus enum classes and composite types, namespaced under a per-schema object. - Idiomatic touches: @SerialName only when the camelCased property name differs from the column name, '= null' defaults on nullable properties, column-less relations emitted as plain @serializable class (a data class with no params is illegal in Kotlin), backtick-escaped hard keywords, and an import block scoped to the symbols actually used. - Wires the /generators/kotlin route, server type-gen dispatch, the PG_META_GENERATE_TYPES_KOTLIN_VISIBILITY env var (public|internal, defaults to public), and a gen:types:kotlin npm script. - Adds typegen tests for default and internal visibility.
A type entry's `format` is the spelled-out SQL name (e.g. `integer`) while its `name` is the catalog name (e.g. `int4`) that the type map and column metadata are keyed on. Composite attributes were resolved via `format`, so scalar attributes like int4/int8 fell through to the JsonElement fallback. Resolve via `name` instead, and flag the JsonElement import on the unresolved-type fallback path.
End-to-end verification against a live Postgres 15Ran the actual object PublicSchema {
@Serializable
enum class UserStatus {
ACTIVE,
@SerialName("not-valid id") not_valid_id,
}
@Serializable
class EmptyTableSelect
@Serializable
data class ProfilesSelect(
@SerialName("class") val `class`: String,
@SerialName("created_at") val createdAt: String,
@SerialName("home_address") val homeAddress: Address? = null,
val id: Long,
val metadata: JsonElement? = null,
val score: Double,
val status: UserStatus? = null,
val tags: List<String>? = null,
)
// ProfilesInsert / ProfilesUpdate / ActiveProfilesSelect ...
@Serializable
data class Address(
@SerialName("street_name") val streetName: String,
val zip: Int,
)
}The live run caught a bug my isolated test missed: composite attribute types must be resolved by their catalog Heads-up for maintainers: the same latent issue exists in The two |
Proof the output is real, compilable Kotlin (not just well-formed text)Compiled the generated file with the actual Kotlin toolchain — The Then a full JSON round-trip through val row = Json.decodeFromString(PublicSchema.ProfilesSelect.serializer(), json)That one test exercises every non-trivial path, and all pass:
Full chain verified: live Postgres → pg-meta introspection → |
What
Adds a Kotlin target to
gen types, alongside the existing TypeScript, Go, Swift, and Python generators. It follows the same structure asswift.ts(per-schema namespace,Select/Insert/Updatevariants, enums, composite types) but emits idiomatickotlinx.serializationcode.Why
The CLI already proxies
supabase gen types --lang {typescript,go,swift,python}straight through to this service viaPG_META_GENERATE_TYPES. Kotlin is the one major Supabase client ecosystem with no first-party type generation, despite an active KMP/Android user base. This closes that gap with no new runtime dependencies.Example output
Design notes
Modeled on
swift.ts, with a few Kotlin-idiomatic refinements:@SerialNameonly when needed — emitted only where the camelCased property name differs from the raw column name, instead of an always-presentCodingKeysblock.= nulldefaults on nullable properties — soInsert/Updatepayloads can omit them, matching how Supabase Kotlin clients build partial rows.@Serializable class— adata classwith an empty primary constructor is a Kotlin compile error, so empty tables/views are special-cased.class,object,in, …) are backtick-escaped; enum values that aren't valid identifiers are sanitized and preserved via@SerialName.SerialName,JsonElement,JsonObject).int2/4/8 → Short/Int/Long,float4/8 → Float/Double,numeric → Double(no dependency-free KMP-wide decimal; mirrors the Go template),json/jsonb → JsonElement,record → JsonObject, arrays →List<T>, enums/composites → generated types.Visibility is controlled by
PG_META_GENERATE_TYPES_KOTLIN_VISIBILITY(public|internal, defaultpublic) and thevisibilityquery param — analogous to Swift'saccess_control.publicis emitted implicitly (Kotlin's default) to keep output lint-clean.Tests
Added
typegen: kotlinandtypegen: kotlin w/ internal visibilitytotest/server/typegen.ts, mirroring the Swift cases. They use empty inline snapshots that populate on first run.