Search code examples
kotlinkotlinx.serialization

Custom serializer for data class without @Serializable


I'm trying to deserialize a JSON file to a Kotlin data class I cannot control using kotlinx.serialization.

The class looks something along the lines of:

public data class Lesson(
    val uid: String,
    val start: Instant,
    val end: Instant,
    val module: String,
    val lecturers: List<String>,
    val room: String?,
    val type: String?,
    val note: String?
)

The JSON I try to parse looks like this:

{
  "lessons": [
    {
      "uid": "sked.de956040",
      "start": "2020-11-02T13:30:00Z",
      "end": "2020-11-02T16:45:00Z",
      "module": "IT2101-Labor SWE I: Gruppe 1 + 2",
      "lecturers": [
        "Kretzmer"
      ],
      "room": "-",
      "type": "La",
      "note": "Prüfung Online"
    }
  ]
}

This is tried via:

@Serializable
data class ExpectedLessons(
    val lessons: List<Lesson>
)

val decoded = Json.decodeFromString<ExpectedLessons>(text)

Solution

  • As the class Lesson cannot be modified, one cannot add a @Serializable annotation to make (de)serialization work. So you may create two custom serializers to make it work.

    @OptIn(ExperimentalSerializationApi::class)
    @Serializer(forClass = Lesson::class)
    object LessonSerializer : KSerializer<Lesson> {
        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Lesson") {
            element<String>("uid")
            element<String>("start")
            element<String>("end")
            element<String>("module")
            element<List<String>>("lecturers")
            element<String?>("room", isOptional = true)
            element<String?>("type", isOptional = true)
            element<String?>("note", isOptional = true)
        }
    
        override fun serialize(encoder: Encoder, value: Lesson) {
            encoder.encodeStructure(descriptor) {
                encodeStringElement(descriptor, 0, value.uid)
                encodeSerializableElement(descriptor, 1, InstantSerializer, value.start)
                encodeSerializableElement(descriptor, 2, InstantSerializer, value.end)
                encodeStringElement(descriptor, 3, value.module)
                encodeSerializableElement(descriptor, 4, ListSerializer(String.serializer()), value.lecturers)
                encodeNullableSerializableElement(descriptor, 5, String.serializer(), value.room)
                encodeNullableSerializableElement(descriptor, 6, String.serializer(), value.type)
                encodeNullableSerializableElement(descriptor, 7, String.serializer(), value.note)
            }
        }
    
        override fun deserialize(decoder: Decoder): Lesson {
            return decoder.decodeStructure(descriptor) {
                var uid: String? = null
                var start: Instant? = null
                var end: Instant? = null
                var module: String? = null
                var lecturers: List<String> = emptyList()
                var room: String? = null
                var type: String? = null
                var note: String? = null
    
                loop@ while (true) {
                    when (val index = decodeElementIndex(descriptor)) {
                        DECODE_DONE -> break@loop
    
                        0 -> uid = decodeStringElement(descriptor, 0)
                        1 -> start = decodeSerializableElement(descriptor, 1, InstantSerializer)
                        2 -> end = decodeSerializableElement(descriptor, 2, InstantSerializer)
                        3 -> module = decodeStringElement(descriptor, 3)
                        4 -> lecturers = decodeSerializableElement(descriptor, 4, ListSerializer(String.serializer()))
                        5 -> room = decodeNullableSerializableElement(descriptor, 5, String.serializer().nullable)
                        6 -> type = decodeNullableSerializableElement(descriptor, 6, String.serializer().nullable)
                        7 -> note = decodeNullableSerializableElement(descriptor, 7, String.serializer().nullable)
    
                        else -> throw SerializationException("Unexpected index $index")
                    }
                }
    
                Lesson(
                    requireNotNull(uid),
                    requireNotNull(start),
                    requireNotNull(end),
                    requireNotNull(module),
                    lecturers,
                    room,
                    type,
                    note
                )
            }
        }
    }
    
    
    @OptIn(ExperimentalSerializationApi::class)
    @Serializer(forClass = Instant::class)
    object InstantSerializer : KSerializer<Instant> {
        override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
    
        override fun serialize(encoder: Encoder, value: Instant) {
            encoder.encodeString("$value")
        }
    
        override fun deserialize(decoder: Decoder): Instant {
            return Instant.parse(decoder.decodeString())
        }
    }
    

    You may configure the serializers before using them like this:

    @file:UseSerializers(InstantSerializer::class, LessonSerializer::class)