stashymane
stashymane

Reputation: 101

Serializing Java Path with kotlinx.serialization

With kotlinx.serialization, this code will throw an error: println(Json.encodeToString(Path.of("value"))) saying kotlinx.serialization.SerializationException: Class 'WindowsPath' is not registered for polymorphic serialization in the scope of 'Path'.

WindowsPath is internal, therefore I can not register it as a polymorphic subclass (as in this example), only with Path itself, and even a custom KSerializer for Path throws the same exact error.
Is there any way to have Path properly serialize/deserialize without having to store it as a string?

Upvotes: 5

Views: 2538

Answers (1)

Path is an interface, so it's implicitly serializable with the PolymorphicSerializer strategy. This strategy requires you to register serializers for subclasses implementing it, but as you know, in this case it's impossible. There is a default polymorphic serializer, but it affects only deserialization process and it works only when deserializable value is a JSONObject.

And for the following serializer

object PathAsStringSerializer : KSerializer<Path> {
    override val descriptor = PrimitiveSerialDescriptor("Path", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Path) = encoder.encodeString(value.toAbsolutePath().toString())

    override fun deserialize(decoder: Decoder): Path = Path.of(decoder.decodeString())
}
\\Not working
val module = SerializersModule { polymorphicDefault(Path::class) { PathAsStringSerializer } }
val decoded : Path = Json { serializersModule = module }.decodeFromString("C:\\Temp")

it will throw runtime exception kotlinx.serialization.json.internal.JsonDecodingException: Expected class kotlinx.serialization.json.JsonObject as the serialized body of kotlinx.serialization.Polymorphic<Path>, but had class kotlinx.serialization.json.JsonLiteral

So, it can't be serialized in a common way, and there are 3 cases of its serialization/deserialization, which need to be handled:

1. Serialization of simple Path variable

In this case you need to explicitly pass your custom serializer:

val path = Path.of("C:\\Temp")

val message1 = Json.encodeToString(PathAsStringSerializer, path).also { println(it) }
println(Json.decodeFromString(PathAsStringSerializer, message1))

2. Serialization of classes, which use Path as a generic parameter

In this case you need to define separate serializers (you may reference original PathAsStringSerializer) and also explicitly pass them:

object ListOfPathsAsStringSerializer : KSerializer<List<Path>> by ListSerializer(PathAsStringSerializer)

val message2 = Json.encodeToString(ListOfPathsAsStringSerializer, listOf(path)).also { println(it) }
println(Json.decodeFromString(ListOfPathsAsStringSerializer, message2))
@Serializable
data class Box<T>(val item: T)
object BoxOfPathSerializer : KSerializer<Box<Path>> by Box.serializer(PathAsStringSerializer)

val message3 = Json.encodeToString(BoxOfPathSerializer, Box(path)).also { println(it) }
println(Json.decodeFromString(BoxOfPathSerializer, message3))

3. Serialization of classes, which have fields of aforementioned types

In this case you need to add respectful @Serializable(with = ...) annotations for these fields:

@Serializable
data class InnerObject(
    @Serializable(with = ListOfPathsAsStringSerializer::class)
    val list: MutableList<Path> = mutableListOf(),
    @Serializable(with = PathAsStringSerializer::class)
    val path: Path,
    @Serializable(with = BoxOfPathSerializer::class)
    val box: Box<Path>
)

Or just list them once for a whole file:

@file: UseSerializers(PathAsStringSerializer::class, ListOfPathsAsStringSerializer::class, BoxOfPathSerializer::class)

Plugin-generated serializer for this case would be good enough:

val message4 = Json.encodeToString(InnerObject(mutableListOf(path), path, Box(path))).also { println(it) }
println(Json.decodeFromString<InnerObject>(message4))

Upvotes: 2

Related Questions