kotlin.md
August 3, 2026 ยท View on GitHub
The Kotlin target emits Kotlin source only. The compiler does not generate Java files.
Output Layout
For source file addressbook.fdl with package addressbook, Kotlin output is
generated under:
<kotlin_out>/addressbook/- Type files:
AddressBook.kt,Person.kt,Dog.kt,Cat.kt,Animal.kt - Schema module:
AddressbookForyModule.kt
The schema module name is derived from the source file stem. Schemas in the same Kotlin package need distinct generated file names; duplicate generated Kotlin file paths are rejected before files are written.
If option kotlin_package = "..."; is present, the output path and Kotlin
package use that option. Otherwise Kotlin uses the FDL package. A Kotlin import
graph cannot mix default-package schemas with named Kotlin packages.
Registration still uses the FDL package so cross-language type names stay
stable.
Type Generation
Messages generate Kotlin data class declarations by default:
@ForyStruct
public data class Person(
@field:ForyField(id = 1)
public val name: String,
@field:ForyField(id = 7)
public val phones: List<PersonPhoneNumber>,
@field:ForyField(id = 8)
public val pet: Animal,
) {
public fun toBytes(): ByteArray = AddressbookForyModule.getFory().serialize(this)
public companion object {
public fun fromBytes(bytes: ByteArray): Person =
AddressbookForyModule.getFory().deserialize(bytes, Person::class.java)
}
}
Messages that participate in compiler-detected construction cycles generate normal mutable classes so the generated serializer can publish the instance before reading back-references:
@ForyStruct
public class Node() {
@ForyField(id = 1)
public var id: String = ""
@Ref
@ForyField(id = 2)
public var parent: Node? = null
}
Generated Kotlin IDL sources express nullability with Kotlin ?, not Fory
@Nullable, including mutable classes emitted for compiler-detected
construction cycles.
Enums generate Kotlin enum classes with stable Fory enum IDs. Unions generate
sealed classes with @ForyUnion; the Fory-provided Unknown(UnknownCase)
carrier is marked with @ForyUnknownCase. The marker only selects the carrier
and does not add an entry to the schema case table. Schema-defined cases may use
case IDs 0..N and hold a single value property. A typed union must have at
least one non-Unknown case.
package addressbook
import org.apache.fory.annotation.ForyCase
import org.apache.fory.annotation.ForyUnion
import org.apache.fory.annotation.ForyUnknownCase
import org.apache.fory.type.union.UnknownCase
@ForyUnion
public sealed class Animal {
@ForyUnknownCase
public data class Unknown(public val value: UnknownCase) : Animal()
@ForyCase(id = 0)
public data class Dog(public val value: addressbook.Dog) : Animal()
}
Packaged Kotlin output keeps the schema case name and qualifies the payload
type when both have the same simple name. If a target output mode cannot express
a legal qualifier for a conflict, the compiler appends Case to the generated
case class name.
Kotlin int32, int64, uint32, and uint64 fields use xlang varint
encoding by default, so generated Kotlin does not emit @VarInt for the
default case. It emits @Fixed or @Tagged only when the schema requests that
non-default encoding. duration maps to kotlin.time.Duration, and infinite
durations are rejected when encoded. Dense array<float16> and
array<bfloat16> use the Java core Float16Array and BFloat16Array
carriers. Generated Kotlin IDL uses @ArrayType ByteArray for array<int8>,
including nested positions.
Schema Module
Generated schema modules register schema types and resolve KSP-generated
serializers from the target class name. The package-owned helper Fory instance uses
ForyKotlin.builder().withXlang(true) with the schema module installed, so message
toBytes/fromBytes helpers work without caller-managed Fory setup. For
addressbook.fdl:
public object AddressbookForyModule : ForyModule {
private val fory: ThreadSafeFory by lazy {
ForyKotlin.builder()
.withXlang(true)
.withRefTracking(true)
.withModule(this)
.buildThreadSafeFory()
}
internal fun getFory(): ThreadSafeFory = fory
override fun install(fory: Fory) {
KotlinSerializers.registerType(fory, Person::class.java, 100L)
KotlinSerializers.registerSerializer(fory, Person::class.java)
KotlinSerializers.registerUnion(fory, Animal::class.java, 106L)
}
}
registerUnion discovers the generated <Target>_ForySerializer; callers do
not pass a serializer instance.
gRPC Service Companions
With --grpc, Kotlin emits one <ServiceName>GrpcKt.kt per service, not a Java companion. The generated object exposes SERVICE_NAME, service and method descriptors, <ServiceName>CoroutineImplBase, and <ServiceName>CoroutineStub. See Kotlin gRPC for dependencies, KSP setup, and coroutine and Flow usage.