summaryrefslogtreecommitdiff
path: root/formats/json-tests/commonTest/src/kotlinx/serialization/features/GenericCustomSerializerTest.kt
blob: 9c623912965b8e85e6bdcb419b2d46d72048a39b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/*
 * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

package kotlinx.serialization.features

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
import kotlinx.serialization.modules.*
import kotlinx.serialization.test.*
import kotlin.test.*

class CheckedData<T : Any>(val data: T, val checkSum: ByteArray) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || this::class != other::class) return false

        other as CheckedData<*>

        if (data != other.data) return false
        if (!checkSum.contentEquals(other.checkSum)) return false

        return true
    }

    override fun hashCode(): Int {
        var result = data.hashCode()
        result = 31 * result + checkSum.contentHashCode()
        return result
    }
}

class CheckedDataSerializer<T : Any>(private val dataSerializer: KSerializer<T>) : KSerializer<CheckedData<T>> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("CheckedDataSerializer") {
        val dataDescriptor = dataSerializer.descriptor
        element("data", dataDescriptor)
        element("checkSum", ByteArraySerializer().descriptor)
    }

    override fun serialize(encoder: Encoder, value: CheckedData<T>) {
        val out = encoder.beginStructure(descriptor)
        out.encodeSerializableElement(descriptor, 0, dataSerializer, value.data)
        out.encodeStringElement(descriptor, 1, InternalHexConverter.printHexBinary(value.checkSum))
        out.endStructure(descriptor)
    }

    override fun deserialize(decoder: Decoder): CheckedData<T> {
        val inp = decoder.beginStructure(descriptor)
        lateinit var data: T
        lateinit var sum: ByteArray
        loop@ while (true) {
            when (val i = inp.decodeElementIndex(descriptor)) {
                CompositeDecoder.DECODE_DONE -> break@loop
                0 -> data = inp.decodeSerializableElement(descriptor, i, dataSerializer)
                1 -> sum = InternalHexConverter.parseHexBinary(inp.decodeStringElement(descriptor, i))
                else -> throw SerializationException("Unknown index $i")
            }
        }
        inp.endStructure(descriptor)
        return CheckedData(data, sum)
    }
}

@Serializable
data class DataWithString(@Serializable(with = CheckedDataSerializer::class) val data: CheckedData<String>)

@Serializable
data class DataWithInt(@Serializable(with = CheckedDataSerializer::class) val data: CheckedData<Int>)

@Serializable
data class DataWithStringContext(@Contextual val data: CheckedData<String>)


@Serializable
data class OptionalHolder(val optionalInt: Optional<Int>)

@Serializable(OptionalSerializer::class)
sealed class Optional<out T : Any?> {
    object NotPresent : Optional<Nothing>()
    data class Value<T : Any?>(val value: T?) : Optional<T>()

    fun get(): T? {
        return when (this) {
            NotPresent -> null
            is Value -> this.value
        }
    }
}

class OptionalSerializer<T>(
    private val valueSerializer: KSerializer<T>
) : KSerializer<Optional<T>> {
    override val descriptor: SerialDescriptor = valueSerializer.descriptor

    override fun deserialize(decoder: Decoder): Optional<T> {
        return try {
            Optional.Value(valueSerializer.deserialize(decoder))
        } catch (exception: Exception) {
            Optional.NotPresent
        }
    }

    override fun serialize(encoder: Encoder, value: Optional<T>) {
        val msg = "Tried to serialize an optional property that had no value present. Is encodeDefaults false?"
        when (value) {
            Optional.NotPresent -> throw SerializationException(msg)
            is Optional.Value ->
                when (val optional = value.value) {
                    null -> encoder.encodeNull()
                    else -> valueSerializer.serialize(encoder, optional)
                }
        }
    }
}


class GenericCustomSerializerTest {
    @Test
    fun testStringData() {
        val original = DataWithString(CheckedData("my data", byteArrayOf(42, 32)))
        val s = Json.encodeToString(DataWithString.serializer(), original)
        assertEquals("""{"data":{"data":"my data","checkSum":"2A20"}}""", s)
        val restored = Json.decodeFromString(DataWithString.serializer(), s)
        assertEquals(original, restored)
    }

    @Test
    fun testIntData() {
        val original = DataWithInt(CheckedData(42, byteArrayOf(42)))
        val s = Json.encodeToString(DataWithInt.serializer(), original)
        assertEquals("""{"data":{"data":42,"checkSum":"2A"}}""", s)
        val restored = Json.decodeFromString(DataWithInt.serializer(), s)
        assertEquals(original, restored)
    }


    @Test
    fun testContextualGeneric() {
        val module = SerializersModule {
            @Suppress("UNCHECKED_CAST")
            contextual(CheckedData::class) { args -> CheckedDataSerializer(args[0] as KSerializer<Any>) }
        }
        assertStringFormAndRestored(
            """{"data":{"data":"my data","checkSum":"2A20"}}""",
            DataWithStringContext(CheckedData("my data", byteArrayOf(42, 32))),
            DataWithStringContext.serializer(),
            Json { serializersModule = module }
        )
    }

    @Test
    fun testOnSealedClass() {
        /*
        Test on custom serializer for sealed class with generic parameter.
        Related issues:
             https://github.com/Kotlin/kotlinx.serialization/issues/1705
             https://youtrack.jetbrains.com/issue/KT-50764
             https://youtrack.jetbrains.com/issue/KT-50718
             https://github.com/Kotlin/kotlinx.serialization/issues/1843
         */
        val encoded = Json.encodeToString(OptionalHolder(Optional.Value(42)))
        assertEquals("""{"optionalInt":42}""", encoded)
    }
}