In Kotlin, deserialize untrusted data with kotlinx.serialization using JSON and sealed class hierarchies, and avoid Java's ObjectInputStream for anything from outside your trust boundary. If legacy code must read Java serialized objects, set an allowlist serialization filter with resource limits before reading.
Which Kotlin deserialization paths are risky?
Kotlin runs on the JVM, so it inherits Java's mechanisms. There are two families.
The first is native to Kotlin: kotlinx.serialization. Its README describes a compiler plugin that generates visitor code for classes marked @Serializable, with JSON, Protobuf, CBOR, Hocon and Properties formats. Its polymorphism guide states that it is fully static with respect to types by default, so the shape of decoded objects comes from compile time types.
The second is Java object serialization, ObjectInputStream, and libraries built on it. This is the classic source of deserialization gadget chains. MITRE's CWE-502 describes the weakness: untrusted data deserialized without ensuring the result is valid, and lists avoiding unnecessary gadget types and using allowlists among mitigations, with the note that new gadgets keep being found, so an allowlist alone is not enough.
How do you use kotlinx.serialization safely?
Constructed example: a webhook accepts events of several kinds.
@Serializable
sealed class Event {
abstract val id: String
}
@Serializable
@SerialName("created")
class Created(override val id: String, val owner: String) : Event()
@Serializable
@SerialName("deleted")
class Deleted(override val id: String) : Event()
val event = Json.decodeFromString<Event>(body)
The kotlinx.serialization docs explain that with a sealed base class, all subclasses must be explicitly @Serializable and are registered automatically. The payload can only select among the subclasses you listed. An unknown type name fails decoding. That is the safe pattern for polymorphism.
Be careful with open polymorphism. Registering many subclasses in a SerializersModule for an open base class widens what the input may select. Register only what the endpoint needs, and use separate hierarchies for separate endpoints.
Should you ignore unknown keys?
By default the Json format is strict with respect to invalid input, per the docs. Setting ignoreUnknownKeys = true makes the decoder accept extra properties and drop them. That is convenient for forward compatibility with trusted partners, but on a public endpoint it hides probing and typos. Keep the default there, and relax it per class where you need to.
What about size and depth?
kotlinx.serialization does not replace a body size limit. Set one at the HTTP layer (your web framework or reverse proxy) so the parser never sees an enormous payload. Then validate values after decoding: lengths, ranges, allowed characters, in init blocks with require(...) or in a separate validation step.
What if you must read Java serialized objects?
Prefer to remove them. Replace the wire format with JSON or Protobuf. If the code is a third party dependency or a legacy protocol you cannot change yet, use the JDK's serialization filtering. Oracle's documentation describes pattern based filters and custom filters, filter factories, and allowlists and rejectlists. OWASP's deserialization cheat sheet advises configuring serialization filters with an application specific class allowlist and resource limits before reading objects, and warns that class checks alone do not bound resource consumption.
Set the filter process wide where possible, so a library that creates its own stream is still covered. Log rejected classes and treat spikes as signals.
Also check frameworks that hide ObjectInputStream: session replication, caching, message brokers, remoting and some RPC libraries. A Kotlin service often pulls these in transitively.
What to do
- Inventory every place you decode data from outside: HTTP, queues, caches, files, session stores.
- Use kotlinx.serialization with sealed classes for polymorphic JSON.
- Keep Json strict on public endpoints; avoid
ignoreUnknownKeysthere. - Enforce body size limits before decoding, and validate fields after.
- Remove ObjectInputStream from trust boundary code.
- Where you cannot, set an allowlist serialization filter with limits.
- Scan dependencies for known gadget libraries and keep them updated.
Safeguard's SBOM and dependency scanning can list which Java libraries in your Gradle build are known to ship deserialization advisories, and reachability analysis shows whether your code uses them.
Quick check
Is kotlinx.serialization vulnerable to gadget chains?
Its design is static: decoded types come from compile time declarations, not from class names in the input, which avoids the classic gadget mechanism. Still validate sizes and values, and keep the library updated.
Does an allowlist fully protect ObjectInputStream?
No. CWE-502 notes new gadgets are found regularly, so allowlists reduce risk but are not sufficient alone. Combine with limits, patching and replacing the format.
Sources
- CWE-502: Deserialization of Untrusted Data, MITRE, fetched 18 October 2026
- kotlinx.serialization repository, GitHub, fetched 18 October 2026
- Polymorphism guide, kotlinx.serialization docs, fetched 18 October 2026
- JSON features, kotlinx.serialization docs, fetched 18 October 2026
- Deserialization Cheat Sheet, OWASP, fetched 18 October 2026
- Serialization filtering, Oracle, fetched 18 October 2026
Last reviewed: 18 October 2026.