Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
like image 659
Endzeit Avatar asked Dec 13 '20 04:12

Endzeit


People also ask

How do you make a data class serializable?

First, make a class serializable by annotating it with @Serializable . You can now serialize an instance of this class by calling Json. encodeToString() . You can also serialize object collections, such as lists, in a single call.

Is string serializable in Kotlin?

This is still the case in Kotlin 1.2! Indeed, both String an Int are internally transformed into appropriate Java classes which are serializeable.

How do you deserialize an object in Kotlin?

Using Google's Gson library We can even perform serialization and deserialization of Kotlin objects using the Gson library. Gson provide functions toJson() and fromJson() to convert Kotlin objects to their JSON representation and vice-versa.


Video Answer


1 Answers

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)
like image 102
Endzeit Avatar answered Oct 24 '22 02:10

Endzeit