1. Introduction

In many real-world applications, we often encounter scenarios where data needs to be deserialized into enums, but the data source might include values not present in our enum definition. This situation can lead to runtime errors if not handled properly.

In this tutorial, we’ll explore safely deserializing enums while ignoring unknown values in Kotlin. We’ll look at three popular libraries for this task: kotlinx.serialization, Jackson, and Gson.

2. Using kotlinx.serialization

kotlinx.serialization is a powerful and flexible serialization library for Kotlin. It provides easy-to-use annotations and functions to handle various serialization and deserialization scenarios.

First, let’s define a simple enum that we’ll use for all examples:

enum class Status {
    SUCCESS,
    ERROR,
    UNKNOWN
}

To handle unknown values, let’s create a custom serializer for our enum:

object StatusSerializer : KSerializer<Status> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Status", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Status) {
        encoder.encodeString(value.name)
    }

    override fun deserialize(decoder: Decoder): Status {
        return try {
            Status.valueOf(decoder.decodeString().uppercase())
        } catch (e: IllegalArgumentException) {
            Status.UNKNOWN
        }
    }
}

This custom serializer attempts to deserialize a string into a Status enum. The deserializer defaults to Status.UNKNOWN if it doesn’t match any known value.

Now, let’s make a simple data class to use this custom serializer with:

@Serializable
data class ApiResponse(
    @Serializable(with = StatusSerializer::class)
    val status: Status
)

First, let’s deserialize a valid value for our enum:

@Test
fun `test known status value deserialization`() {
    val json = """{"status": "success"}"""
    val response = Json.decodeFromString(json)
    assertEquals(Status.SUCCESS, response.status)
}

And now let’s try an invalid value for our enum to make sure we get our default UNKNOWN value:

@Test
fun `test unknown status value deserialization`() {
    val unknownJson = """{"status": "unknown_value"}"""
    val unknownResponse = Json.decodeFromString(unknownJson)
    assertEquals(Status.UNKNOWN, unknownResponse.status)
}

3. Using Jackson

Jackson is another widely used library for JSON processing. It provides robust mechanisms to handle unknown enum values during deserialization.

With Jackson, we can use the @JsonEnumDefaultValue annotation directly in the enum definition. For that, we must modify our default enum entry to use the annotation:

enum class Status {
    SUCCESS,
    ERROR,
    @JsonEnumDefaultValue
    UNKNOWN
} 

Jackson allows us to use annotations and configuration options to handle unknown values. We can configure the ObjectMapper to use a fallback value for unknown enum constants:

val mapper = jacksonObjectMapper().apply {
    configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true)
}

Again we’ll first test with an enum value that we know exists:

@Test
fun `test known status value deserialization`() {
    val json = """{"status": "SUCCESS"}"""
    val response = mapper.readValue(json)
    assertEquals(Status.SUCCESS, response.status)
}

Then we can also test with an enum value that doesn’t exist to ensure we get our default:

@Test
fun `test unknown status value deserialization`() {
    val unknownJson = """{"status": "unknown_value"}"""
    val response = mapper.readValue(unkownJson)
    assertEquals(Status.UNKNOWN, response.status)
}

4. Using Gson

Gson is another popular library for JSON serialization and deserialization.

We’ll also need to create a custom deserializer to handle this with Gson:

class StatusDeserializer : JsonDeserializer<Status> {
    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Status {
        return try {
            Status.valueOf(json.asString.uppercase())
        } catch (e: IllegalArgumentException) {
            Status.UNKNOWN
        }
    }
}

Then we configure our Gson instance to use the custom deserializer:

val gson = GsonBuilder()
    .registerTypeAdapter(Status::class.java, StatusDeserializer())
    .create()

Finally, here’s how to test the deserialization with Gson with a valid value first:

@Test
fun `test known status value deserialization`() {
    val json = """{"status": "SUCCESS"}"""
    val response = gson.fromJson(json, ApiResponse::class.java)
    assertEquals(Status.SUCCESS, response.status)
}

And then let’s try with an unknown value to test our default:

@Test
fun `test unknown status value deserialization`() {
    val unknownJson = """{"status": "unknown_value"}"""
    val response = gson.fromJson(unknownJson, ApiResponse::class.java)
    assertEquals(Status.UNKNOWN, response.status)
}

5. Conclusion

Handling unknown enum values during deserialization is crucial for building robust applications that gracefully handle unexpected data. In this article, we saw how to achieve this using kotlinx.serialization, Jackson, and Gson in Kotlin. Jackson is the only solution with something built in to handle this, but we can achieve similar results with custom serializers in kotlinx.serialization and Gson. This ensures our applications remain resilient even when encountering unknown values.

As always, the code used in this article is available over on GitHub.