title: Kotlin sidebar_position: 11 id: kotlin license: | Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
The Kotlin target emits Kotlin source only. The compiler does not generate Java files.
For source file addressbook.fdl with package addressbook, Kotlin output is generated under:
<kotlin_out>/addressbook/AddressBook.kt, Person.kt, Dog.kt, Cat.kt, Animal.ktAddressbookForyModule.ktThe 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.
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.
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.
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.