
Enables enum decoding to map unknown input values to a designated @Fallback entry instead of failing, with generated serializers, SerialInfo support, collection-safe decoding and build-time checks.
A Kotlin compiler plugin which works alongside kotlinx.serialization to make enum decoding more forgiving. Mark one enum entry with @Fallback and any unknown name in your input data will decode to it, instead of throwing SerializationException.
When the data you decode can contain enum values your code doesn't know about yet. For example:
kotlinx.serialization's coerceInputValues does something similar, but it only covers class properties that have a default value. This plugin is applied to the enum type, so it works anywhere the enum is decoded, including top-level values and collections. One unknown value in a list doesn't stop the rest of it from decoding.
In settings.gradle.kts:
pluginManagement {
repositories {
mavenCentral()
}
}In build.gradle.kts:
plugins {
kotlin("multiplatform") // or kotlin("jvm"), etc.
kotlin("plugin.serialization")
id("dev.jonpoulton.fallbackserializer") version "<version>"
}The Gradle plugin adds the dev.jonpoulton.fallbackserializer:runtime dependency for you, which includes:
Fallback annotation, to be applied to your enum entriesFallbackEnumSerializer class, used as a base class for compiler-generated serializer types. You shouldn't need to touch this yourself.The runtime supports every platform that kotlinx.serialization does.
The plugin needs the K2 compiler. Compiler plugins use internal compiler APIs, so each release only supports the Kotlin versions it's tested against:
| Plugin | Kotlin |
|---|---|
| 0.1.0 | 2.4.20 |
| 0.2.0 | 2.4.20 |
| 0.3.0 | 2.4.0, 2.4.10, 2.4.20 |
The Gradle plugin warns on any other Kotlin version, since the compiler plugin might not work with it. To hide the warning, set fallback.skipKotlinVersionCheck=true in gradle.properties.
import fallback.serializer.Fallback
// The serializer is generated by the plugin and applied automatically, so a plain @Serializable is
// all you need. OrderStatus.serializer() returns it if you want to pass it explicitly.
@Serializable
enum class OrderStatus {
Pending,
@SerialName("in_transit") Shipped,
@Fallback Unknown,
}
Json.decodeFromString<OrderStatus>("\"Pending\"") // -> OrderStatus.Pending
Json.decodeFromString<OrderStatus>("\"in_transit\"") // -> OrderStatus.Shipped
Json.decodeFromString<OrderStatus>("\"Refunded\"") // -> OrderStatus.UnknownEncoding the fallback entry uses its own name, so the original value is lost:
Json.encodeToString(OrderStatus.Unknown) // -> "Unknown", not "Refunded"The above uses JSON as an example, but this works with other kotlinx.serialization formats too. It's tested with JSON, CBOR, ProtoBuf and XML. With ProtoBuf, which encodes enums as numbers, an unknown number decodes to the fallback.
Only unknown strings decode to the fallback. A value of the wrong type, like 123 or null, still fails to decode, except with Json.decodeFromJsonElement, which decodes it to the fallback too.
Enums without a @Fallback entry are left alone.
@JsonNames and other @SerialInfo annotations on the enum and its entries still work as expected.expect/actual enums, only the actual enum needs the @Fallback entry. Decoding from common code still uses the fallback.coerceInputValues = true, Json swaps an unknown value for the property's default before this plugin sees it. So a property with a default gets that default, and one without a default gets the fallback entry:@Serializable
data class Order(val withDefault: OrderStatus = OrderStatus.Pending, val withoutDefault: OrderStatus)
val json = Json { coerceInputValues = true }
json.decodeFromString<Order>("""{"withDefault":"Refunded","withoutDefault":"Refunded"}""")
// -> Order(withDefault = OrderStatus.Pending, withoutDefault = OrderStatus.Unknown)The plugin includes a number of built-in usage checkers which make sure the @Fallback annotation is being applied properly. These will fail the build in any of the following cases:
@Fallback entry.@Fallback entry but isn't annotated with @Serializable (or an annotation marked with @MetaSerializable), which would otherwise ignore the fallback without telling you.@Fallback entry passes a with argument to @Serializable, since that serializer would be used instead of the generated one and the fallback would be ignored.@Fallback entry, but its expect enum does.@Fallback.@Fallback is used anywhere besides an enum entry.If you find any other cases that should be caught, please open an issue.
Copyright 2026 Jon Poulton
Licensed 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
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
A Kotlin compiler plugin which works alongside kotlinx.serialization to make enum decoding more forgiving. Mark one enum entry with @Fallback and any unknown name in your input data will decode to it, instead of throwing SerializationException.
When the data you decode can contain enum values your code doesn't know about yet. For example:
kotlinx.serialization's coerceInputValues does something similar, but it only covers class properties that have a default value. This plugin is applied to the enum type, so it works anywhere the enum is decoded, including top-level values and collections. One unknown value in a list doesn't stop the rest of it from decoding.
In settings.gradle.kts:
pluginManagement {
repositories {
mavenCentral()
}
}In build.gradle.kts:
plugins {
kotlin("multiplatform") // or kotlin("jvm"), etc.
kotlin("plugin.serialization")
id("dev.jonpoulton.fallbackserializer") version "<version>"
}The Gradle plugin adds the dev.jonpoulton.fallbackserializer:runtime dependency for you, which includes:
Fallback annotation, to be applied to your enum entriesFallbackEnumSerializer class, used as a base class for compiler-generated serializer types. You shouldn't need to touch this yourself.The runtime supports every platform that kotlinx.serialization does.
The plugin needs the K2 compiler. Compiler plugins use internal compiler APIs, so each release only supports the Kotlin versions it's tested against:
| Plugin | Kotlin |
|---|---|
| 0.1.0 | 2.4.20 |
| 0.2.0 | 2.4.20 |
| 0.3.0 | 2.4.0, 2.4.10, 2.4.20 |
The Gradle plugin warns on any other Kotlin version, since the compiler plugin might not work with it. To hide the warning, set fallback.skipKotlinVersionCheck=true in gradle.properties.
import fallback.serializer.Fallback
// The serializer is generated by the plugin and applied automatically, so a plain @Serializable is
// all you need. OrderStatus.serializer() returns it if you want to pass it explicitly.
@Serializable
enum class OrderStatus {
Pending,
@SerialName("in_transit") Shipped,
@Fallback Unknown,
}
Json.decodeFromString<OrderStatus>("\"Pending\"") // -> OrderStatus.Pending
Json.decodeFromString<OrderStatus>("\"in_transit\"") // -> OrderStatus.Shipped
Json.decodeFromString<OrderStatus>("\"Refunded\"") // -> OrderStatus.UnknownEncoding the fallback entry uses its own name, so the original value is lost:
Json.encodeToString(OrderStatus.Unknown) // -> "Unknown", not "Refunded"The above uses JSON as an example, but this works with other kotlinx.serialization formats too. It's tested with JSON, CBOR, ProtoBuf and XML. With ProtoBuf, which encodes enums as numbers, an unknown number decodes to the fallback.
Only unknown strings decode to the fallback. A value of the wrong type, like 123 or null, still fails to decode, except with Json.decodeFromJsonElement, which decodes it to the fallback too.
Enums without a @Fallback entry are left alone.
@JsonNames and other @SerialInfo annotations on the enum and its entries still work as expected.expect/actual enums, only the actual enum needs the @Fallback entry. Decoding from common code still uses the fallback.coerceInputValues = true, Json swaps an unknown value for the property's default before this plugin sees it. So a property with a default gets that default, and one without a default gets the fallback entry:@Serializable
data class Order(val withDefault: OrderStatus = OrderStatus.Pending, val withoutDefault: OrderStatus)
val json = Json { coerceInputValues = true }
json.decodeFromString<Order>("""{"withDefault":"Refunded","withoutDefault":"Refunded"}""")
// -> Order(withDefault = OrderStatus.Pending, withoutDefault = OrderStatus.Unknown)The plugin includes a number of built-in usage checkers which make sure the @Fallback annotation is being applied properly. These will fail the build in any of the following cases:
@Fallback entry.@Fallback entry but isn't annotated with @Serializable (or an annotation marked with @MetaSerializable), which would otherwise ignore the fallback without telling you.@Fallback entry passes a with argument to @Serializable, since that serializer would be used instead of the generated one and the fallback would be ignored.@Fallback entry, but its expect enum does.@Fallback.@Fallback is used anywhere besides an enum entry.If you find any other cases that should be caught, please open an issue.
Copyright 2026 Jon Poulton
Licensed 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
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.