Skip to main content
Version: dev

Static Generated Serializers

Use fory-kotlin-ksp when Kotlin classes must participate in Fory cross-language schema serialization. The processor generates Kotlin source serializers at build time. Those serializers call the existing Fory Java runtime, including WriteContext, ReadContext, and MemoryBuffer; there is no Kotlin-only protocol.

Static generated Kotlin serializers are for Kotlin/JVM and Android xlang/schema mode. They are not Java native object serializers and do not preserve JVM object graph implementation details such as the exact concrete collection class.

Add KSP

Add fory-kotlin at runtime and run fory-kotlin-ksp as a KSP processor in the module that compiles your @ForyStruct Kotlin classes.

plugins {
id("com.google.devtools.ksp") version "<ksp-version>"
}

dependencies {
implementation("org.apache.fory:fory-kotlin:<fory-version>")
ksp("org.apache.fory:fory-kotlin-ksp:<fory-version>")
}

For Android, configure KSP in the Android module or library module that owns the Kotlin model classes.

Define A Struct

Reuse the Java Fory annotations for schema concepts. Use Kotlin type-use annotations only when you need to override integer encoding.

import org.apache.fory.annotation.ForyField
import org.apache.fory.annotation.ForyStruct
import org.apache.fory.kotlin.Fixed
import org.apache.fory.kotlin.VarInt

@ForyStruct
data class User(
@ForyField(id = 1)
val id: @Fixed UInt,

@ForyField(id = 2)
val score: @VarInt Long,

@ForyField(id = 3)
val tags: List<String>,
)

Use @ForyField(id = 1) on constructor properties. @field:ForyField(id = 1) is also accepted for field-backed properties. Do not use @get:ForyField or @set:ForyField; accessors are not schema fields and the processor rejects them.

Supported Structs

The processor generates serializers for public or internal, concrete, non-generic classes in named packages. A supported class must have a primary constructor whose serialized parameters are val or var properties. data class is the common case, but it is not required.

Internal Kotlin struct classes are supported when KSP runs in the same Kotlin module that owns the struct. The generated Kotlin serializer is also internal, so it can call the internal constructor and expose the internal type in overrides while still producing a JVM class that the Fory Java runtime can load. Application code outside that Kotlin module still cannot refer to the internal struct directly, so registration must happen from code that can see the class.

The processor rejects these declarations:

  • private struct classes.
  • local, anonymous, or nested @ForyStruct classes.
  • Kotlin object declarations.
  • interfaces, abstract classes, and sealed classes as serializer targets.
  • generic @ForyStruct classes.
  • private constructor properties.
  • private or protected primary constructors.

Kotlin default constructor arguments are supported for compatible reads. A struct can have up to 12 defaulted constructor fields.

Constructor-based generated serializers support wide primary constructors. Compatible reads track remote field presence in generated side state instead of using constructor bit masks.

Nullability

Use Kotlin ? to describe nullable schema positions. Nullability is preserved inside collections and maps.

@ForyStruct
data class NullabilityExample(
@ForyField(id = 1)
val a: List<String>,

@ForyField(id = 2)
val b: List<String?>,

@ForyField(id = 3)
val c: List<String>?,

@ForyField(id = 4)
val d: List<String?>?,
)

Do not use Fory @Nullable in hand-written constructor-based Kotlin structs. The KSP processor rejects it so the schema is always read from Kotlin source nullability. Compiler-generated Kotlin IDL sources follow the same rule and use Kotlin ? for nullable fields.

References

Kotlin generated serializers preserve @Ref metadata for fields, list elements, and map values. Constructor-owned reads construct Kotlin values through primary constructors. Schema IDL classes that need reference publication are emitted as mutable no-arg classes, and their KSP-generated serializers publish the instance before reading fields. In both shapes, KSP owns field descriptors, nested nullability, and @Ref metadata.

Collections

Collection declarations carry schema shape, not JVM implementation identity. For example, List<String> is encoded as list<string> and Map<String, Int> is encoded as map<string, int32>.

Deserialization only guarantees that the result is assignable to the declared field type. Fory does not preserve whether the original runtime value was an ArrayList, LinkedList, Collections.unmodifiableList, synchronized collection wrapper, or another JVM-specific collection implementation.

Supported collection declarations include Kotlin and Java list, set, and map types. Mutable collection interface fields are deserialized to mutable implementations assignable to the declared type. Sorted collections without an explicit comparator, such as TreeSet and ConcurrentSkipListSet, are accepted only for non-null scalar or string elements. Concurrent map declarations are accepted only with non-null values because JVM concurrent map implementations reject null entries.

Set<*>, Map<*, T>, Map<*, *>, and raw Java collections are rejected. List<*> and Map<K, *> are accepted and use dynamic nullable values.

Dense Arrays

Kotlin dense primitive and unsigned array fields are supported:

  • BooleanArray
  • ByteArray
  • ShortArray
  • IntArray
  • LongArray
  • FloatArray
  • DoubleArray
  • UByteArray
  • UShortArray
  • UIntArray
  • ULongArray

Dense arrays with unambiguous Kotlin carriers are supported in fields, collection elements, map values, and union cases. array<float16> and array<bfloat16> use the Java core Float16Array and BFloat16Array carriers.

ByteArray is encoded as Fory binary unless the ByteArray type use is annotated with Java @ArrayType. Generated Kotlin IDL uses @ArrayType ByteArray for array<int8>, including nested collection and map positions.

@ArrayType is also supported on top-level List<T> fields when T is a non-null boolean or numeric dense-array element type. In that case the field is encoded as dense array<T> schema, and generated reads convert decoded JVM list elements back to the declared Kotlin element carrier.

Integer Encoding

Kotlin type-use encoding annotations map to Fory xlang integer encodings:

AnnotationValid Kotlin types
@FixedInt, Long, UInt, ULong
@VarIntInt, Long, UInt, ULong
@TaggedLong, ULong

Without an annotation, xlang Int, Long, UInt, and ULong use varint encoding. This is required by xlang mode and is not controlled by Java native mode numeric compression options.

Duration

Xlang duration maps to kotlin.time.Duration. Infinite Kotlin durations cannot be represented by the xlang duration payload and fail during serialization.

Sealed Unions

KSP generates serializers for top-level sealed classes annotated with @ForyUnion. Each schema case is a nested class annotated with @ForyCase and one constructor property named value. Case ID 0 is reserved for the unknown case carrier:

@ForyUnion
sealed class Animal {
@ForyCase(id = 0)
data class UnknownCase(val caseId: Int, val value: Any?) : Animal()

@ForyCase(id = 1)
data class DogCase(val value: Dog) : Animal()
}

Generated schema modules register sealed unions through KotlinSerializers.registerUnion. The runtime discovers the generated <Target>_ForySerializer automatically, so callers do not pass a serializer instance.

Register Classes

Register Kotlin struct classes with the Kotlin register<T> extension. You choose the xlang namespace and type name; generated serializers do not choose IDs or names for you.

import org.apache.fory.kotlin.ForyKotlin
import org.apache.fory.kotlin.register

val fory = ForyKotlin.builder()
.withXlang(true)
.requireClassRegistration(true)
.build()

fory.register<User>("example", "User")

ForyKotlin.builder() installs the Kotlin runtime bootstrap for the Fory instance. The fory.register<T>(...) extension registers your xlang schema type name and resolves the generated serializer from the target class.

Do not register or reference generated serializer classes in application code. The runtime resolves them from the registered target class.

Generated Schema IDL modules use the same path. They call KotlinSerializers.registerType, registerSerializer, registerEnum, and registerUnion as appropriate and never emit Java files.

Generated Names

The generated serializer is emitted in the same package as the target class. Its name is <target>_ForySerializer. For nested binary names, $ is encoded as _; source underscores are encoded as _u_.

These names are an implementation detail. They matter for diagnostics and Android shrinking, but user code should only register target classes.

If a constructor-owned Kotlin xlang struct is registered but its KSP generated serializer is missing, Fory fails with a configuration error. Compile generated IDL sources with KSP before registering generated Kotlin classes.

Android And R8

Android apps should not need user-written keep rules for generated Kotlin serializers. KSP emits generated consumer R8/ProGuard rules under META-INF/proguard/ for the generated serializer constructors used by Fory and the Kotlin metadata needed to detect required Kotlin generated serializers.

For library modules, package the generated META-INF/proguard/ resources into the produced artifact. For Android application modules, make sure your KSP setup includes generated resources in the minified variant.

See Android Support for Android Gradle setup and release-minified validation guidance.

Native Object Mode

Kotlin KSP generated serializers are only for xlang/schema mode. They do not replace Fory Java native object serializers and do not preserve JVM object graph identity. If you use Fory with withXlang(false), Fory uses the normal Java and Kotlin runtime serializers instead.

Kotlin/Native and Kotlin/JS are not supported by this module.