diff options
Diffstat (limited to 'formats/json-tests/jvmTest/src/kotlinx')
22 files changed, 1932 insertions, 0 deletions
diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt new file mode 100644 index 00000000..a0c4c73a --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/BigDecimalTest.kt @@ -0,0 +1,193 @@ +package kotlinx.serialization + +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.* +import org.junit.Test +import java.math.BigDecimal + +private typealias BigDecimalKxs = @Serializable(with = BigDecimalNumericSerializer::class) BigDecimal + +class BigDecimalTest : JsonTestBase() { + + private val json = Json { + prettyPrint = true + } + + private inline fun <reified T> assertBigDecimalJsonFormAndRestored( + expected: String, + actual: T, + serializer: KSerializer<T> = serializer(), + ) = assertJsonFormAndRestored( + serializer, + actual, + expected, + json + ) + + @Test + fun bigDecimal() { + fun test(expected: String, actual: BigDecimal) = + assertBigDecimalJsonFormAndRestored(expected, actual, BigDecimalNumericSerializer) + + test("0", BigDecimal.ZERO) + test("1", BigDecimal.ONE) + test("-1", BigDecimal("-1")) + test("10", BigDecimal.TEN) + test(bdExpected1, bdActual1) + test(bdExpected2, bdActual2) + test(bdExpected3, bdActual3) + test(bdExpected4, bdActual4) + test(bdExpected5, bdActual5) + test(bdExpected6, bdActual6) + } + + @Test + fun bigDecimalList() { + + val bdList: List<BigDecimal> = listOf( + bdActual1, + bdActual2, + bdActual3, + bdActual4, + bdActual5, + bdActual6, + ) + + val expected = + """ + [ + $bdExpected1, + $bdExpected2, + $bdExpected3, + $bdExpected4, + $bdExpected5, + $bdExpected6 + ] + """.trimIndent() + + assertJsonFormAndRestored( + ListSerializer(BigDecimalNumericSerializer), + bdList, + expected, + json, + ) + } + + @Test + fun bigDecimalMap() { + val bdMap: Map<BigDecimal, BigDecimal> = mapOf( + bdActual1 to bdActual2, + bdActual3 to bdActual4, + bdActual5 to bdActual6, + ) + + val expected = + """ + { + "$bdExpected1": $bdExpected2, + "$bdExpected3": $bdExpected4, + "$bdExpected5": $bdExpected6 + } + """.trimIndent() + + assertJsonFormAndRestored( + MapSerializer(BigDecimalNumericSerializer, BigDecimalNumericSerializer), + bdMap, + expected, + json, + ) + } + + @Test + fun bigDecimalHolder() { + val bdHolder = BigDecimalHolder( + bd = bdActual1, + bdList = listOf( + bdActual1, + bdActual2, + bdActual3, + ), + bdMap = mapOf( + bdActual1 to bdActual2, + bdActual3 to bdActual4, + bdActual5 to bdActual6, + ), + ) + + val expected = + """ + { + "bd": $bdExpected1, + "bdList": [ + $bdExpected1, + $bdExpected2, + $bdExpected3 + ], + "bdMap": { + "$bdExpected1": $bdExpected2, + "$bdExpected3": $bdExpected4, + "$bdExpected5": $bdExpected6 + } + } + """.trimIndent() + + assertBigDecimalJsonFormAndRestored( + expected, + bdHolder, + ) + } + + companion object { + + // test data + private val bdActual1 = BigDecimal("725345854747326287606413621318.311864440287151714280387858224") + private val bdActual2 = BigDecimal("336052472523017262165484244513.836582112201211216526831524328") + private val bdActual3 = BigDecimal("211054843014778386028147282517.011200287614476453868782405400") + private val bdActual4 = BigDecimal("364751025728628060231208776573.207325218263752602211531367642") + private val bdActual5 = BigDecimal("508257556021513833656664177125.824502734715222686411316853148") + private val bdActual6 = BigDecimal("127134584027580606401102614002.366672301517071543257300444000") + + private const val bdExpected1 = "725345854747326287606413621318.311864440287151714280387858224" + private const val bdExpected2 = "336052472523017262165484244513.836582112201211216526831524328" + private const val bdExpected3 = "211054843014778386028147282517.011200287614476453868782405400" + private const val bdExpected4 = "364751025728628060231208776573.207325218263752602211531367642" + private const val bdExpected5 = "508257556021513833656664177125.824502734715222686411316853148" + private const val bdExpected6 = "127134584027580606401102614002.366672301517071543257300444000" + } + +} + +@Serializable +private data class BigDecimalHolder( + val bd: BigDecimalKxs, + val bdList: List<BigDecimalKxs>, + val bdMap: Map<BigDecimalKxs, BigDecimalKxs>, +) + +private object BigDecimalNumericSerializer : KSerializer<BigDecimal> { + + override val descriptor = PrimitiveSerialDescriptor("java.math.BigDecimal", PrimitiveKind.DOUBLE) + + override fun deserialize(decoder: Decoder): BigDecimal { + return if (decoder is JsonDecoder) { + BigDecimal(decoder.decodeJsonElement().jsonPrimitive.content) + } else { + BigDecimal(decoder.decodeString()) + } + } + + override fun serialize(encoder: Encoder, value: BigDecimal) { + val bdString = value.toPlainString() + + if (encoder is JsonEncoder) { + encoder.encodeJsonElement(JsonUnquotedLiteral(bdString)) + } else { + encoder.encodeString(bdString) + } + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/JavaCollectionsTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/JavaCollectionsTest.kt new file mode 100644 index 00000000..1f4958a6 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/JavaCollectionsTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2019 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kotlinx.serialization + +import kotlinx.serialization.json.Json +import kotlinx.serialization.test.typeTokenOf +import org.junit.Test +import java.util.HashMap +import java.util.HashSet +import kotlin.collections.LinkedHashMap +import kotlin.collections.Map +import kotlin.collections.hashMapOf +import kotlin.collections.hashSetOf +import kotlin.reflect.* +import kotlin.test.* + + +class JavaCollectionsTest { + @Serializable + data class HasHashMap( + val s: String, + val hashMap: HashMap<Int, String>, + val hashSet: HashSet<Int>, + val linkedHashMap: LinkedHashMap<Int, String>, + val kEntry: Map.Entry<Int, String>? + ) + + @Test + fun testJavaCollectionsInsideClass() { + val original = HasHashMap("42", hashMapOf(1 to "1", 2 to "2"), hashSetOf(11), LinkedHashMap(), null) + val serializer = HasHashMap.serializer() + val string = Json.encodeToString(serializer = serializer, value = original) + assertEquals( + expected = """{"s":"42","hashMap":{"1":"1","2":"2"},"hashSet":[11],"linkedHashMap":{},"kEntry":null}""", + actual = string + ) + val restored = Json.decodeFromString(deserializer = serializer, string = string) + assertEquals(expected = original, actual = restored) + } + + @Test + fun testTopLevelMaps() { + // Returning null here is a deliberate choice: map constructor functions may return different specialized + // implementations (e.g., kotlin.collections.EmptyMap or java.util.Collections.SingletonMap) + // that may or may not be generic. Since we generally cannot return a generic serializer using Java class only, + // all attempts to get map serializer using only .javaClass should return null. + assertNull(serializerOrNull(emptyMap<String, String>().javaClass)) + assertNull(serializerOrNull(mapOf<String, String>("a" to "b").javaClass)) + assertNull(serializerOrNull(mapOf<String, String>("a" to "b", "b" to "c").javaClass)) + // Correct ways of retrieving map serializer: + assertContains( + serializer(typeTokenOf<Map<String, String>>()).descriptor.serialName, + "kotlin.collections.LinkedHashMap" + ) + assertContains( + serializer(typeTokenOf<java.util.LinkedHashMap<String, String>>()).descriptor.serialName, + "kotlin.collections.LinkedHashMap" + ) + assertContains( + serializer(typeOf<LinkedHashMap<String, String>>()).descriptor.serialName, + "kotlin.collections.LinkedHashMap" + ) + } + + @Test + fun testTopLevelSetsAndLists() { + // Same reasoning as for maps + assertNull(serializerOrNull(emptyList<String>().javaClass)) + assertNull(serializerOrNull(listOf<String>("a").javaClass)) + assertNull(serializerOrNull(listOf<String>("a", "b").javaClass)) + assertNull(serializerOrNull(emptySet<String>().javaClass)) + assertNull(serializerOrNull(setOf<String>("a").javaClass)) + assertNull(serializerOrNull(setOf<String>("a", "b").javaClass)) + assertContains( + serializer(typeTokenOf<Set<String>>()).descriptor.serialName, + "kotlin.collections.LinkedHashSet" + ) + assertContains( + serializer(typeTokenOf<List<String>>()).descriptor.serialName, + "kotlin.collections.ArrayList" + ) + assertContains( + serializer(typeTokenOf<java.util.LinkedHashSet<String>>()).descriptor.serialName, + "kotlin.collections.LinkedHashSet" + ) + assertContains( + serializer(typeTokenOf<java.util.ArrayList<String>>()).descriptor.serialName, + "kotlin.collections.ArrayList" + ) + } + + @Test + fun testAnonymousObject() { + val obj: Any = object {} + assertNull(serializerOrNull(obj.javaClass)) + } +} + diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt new file mode 100644 index 00000000..a423bc84 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/JvmMissingFieldsExceptionTest.kt @@ -0,0 +1,138 @@ +package kotlinx.serialization + +import org.junit.Test +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class JvmMissingFieldsExceptionTest { + @Serializable + data class Generic<out T1, out T2, out T3>(val f1: T1, val f2: T2, val f3: T3) + + @Serializable + sealed class Parent(val p1: Int, val p2: Int = 2, val p3: Int) { + @Serializable + @SerialName("child") + data class Child(val c1: Int = 1, val c2: Int, val c3: Int = 3) : Parent(c1 + 1, 2, 3) + } + + @Serializable + open class ShortPlaneClass(val f1: Int, val f2: Int, val f3: Int = 3, val f4: Int) + + @Serializable + class WithTransient(val f1: Int, @Transient val f2: Int = 2, val f3: Int, val f4: Int) + + @Serializable + abstract class SimpleAbstract(val p1: Int, val p2: Int) + + @Serializable + @SerialName("a") + data class ChildA(val c1: Int, val c2: Int = 2, val c3: Int) : SimpleAbstract(0, 10) + + @Serializable + data class PolymorphicWrapper(@Polymorphic val nested: SimpleAbstract) + + @Serializable + class BigPlaneClass( + val f0: Int, + val f5: Int = 5, + val f6: Int, + val f7: Int = 7, + val f8: Int, + val f9: Int, + val f10: Int, + val f11: Int, + val f12: Int, + val f13: Int, + val f14: Int, + val f15: Int, + val f16: Int, + val f17: Int, + val f18: Int, + val f19: Int, + val f20: Int, + val f21: Int, + val f22: Int, + val f23: Int, + val f24: Int, + val f25: Int, + val f26: Int, + val f27: Int, + val f28: Int, + val f29: Int, + val f30: Int, + val f31: Int, + val f32: Int, + val f33: Int, + val f34: Int, + val f35: Int, + ) : ShortPlaneClass(1, 2, 3, 4) + + @Test + fun testShortPlaneClass() { + assertFailsWithMessages(listOf("f2", "f4")) { + Json.decodeFromString<ShortPlaneClass>("""{"f1":1}""") + } + } + + @Test + fun testBigPlaneClass() { + val missedFields = MutableList(36) { "f$it" } + val definedInJsonFields = arrayOf("f1", "f15", "f34") + val optionalFields = arrayOf("f3", "f5", "f7") + missedFields.removeAll(definedInJsonFields) + missedFields.removeAll(optionalFields) + assertFailsWithMessages(missedFields) { + Json.decodeFromString<BigPlaneClass>("""{"f1":1, "f15": 15, "f34": 34}""") + } + } + + @Test + fun testAnnotatedPolymorphic() { + val module = SerializersModule { + polymorphic(SimpleAbstract::class, null) { + subclass(ChildA::class) + } + } + + assertFailsWithMessages(listOf("p2", "c3")) { + Json { + serializersModule = module + }.decodeFromString<PolymorphicWrapper>("""{"nested": {"type": "a", "p1": 1, "c1": 11}}""") + } + } + + + @Test + fun testSealed() { + assertFailsWithMessages(listOf("p3", "c2")) { + Json.decodeFromString<Parent>("""{"type": "child", "p1":1, "c1": 11}""") + } + } + + @Test + fun testTransient() { + assertFailsWithMessages(listOf("f3", "f4")) { + Json.decodeFromString<WithTransient>("""{"f1":1}""") + } + } + + @Test + fun testGeneric() { + assertFailsWithMessages(listOf("f2", "f3")) { + Json.decodeFromString<Generic<Int, Int, Int>>("""{"f1":1}""") + } + } + + + private inline fun assertFailsWithMessages(fields: List<String>, block: () -> Unit) { + val exception = assertFailsWith(MissingFieldException::class, null, block) + val missedMessages = fields.filter { !exception.message!!.contains(it) } + assertEquals(exception.missingFields.sorted(), fields.sorted()) + assertTrue(missedMessages.isEmpty(), "Expected message '${exception.message}' to contain substrings $fields") + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializationCasesTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializationCasesTest.kt new file mode 100644 index 00000000..cbef36f3 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializationCasesTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2018 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kotlinx.serialization + +import kotlinx.serialization.json.* +import org.junit.* +import org.junit.Assert.* + +class SerializationCasesTest : JsonTestBase() { + + @Serializable + data class Data1(val a: Int, val b: Int) + + @Serializer(forClass = Data1::class) + object ExtDataSerializer1 + + @Test + fun testConstructorValProperties() { + val data = Data1(1, 2) + + // Serialize with internal serializer for Data class + assertEquals("""{"a":1,"b":2}""", default.encodeToString(data)) + assertEquals(data, Json.decodeFromString<Data1>("""{"a":1,"b":2}""")) + + // Serialize with external serializer for Data class + assertEquals("""{"a":1,"b":2}""", default.encodeToString(ExtDataSerializer1, data)) + assertEquals(data, Json.decodeFromString(ExtDataSerializer1, """{"a":1,"b":2}""")) + } + + @Serializable + class Data2 { + var a = 0 + var b = 0 + override fun equals(other: Any?) = other is Data2 && other.a == a && other.b == b + } + + @Serializer(forClass=Data2::class) + object ExtDataSerializer2 + + @Test + fun testBodyVarProperties() { + val data = Data2().apply { + a = 1 + b = 2 + } + + // Serialize with internal serializer for Data class + assertEquals("""{"a":1,"b":2}""", default.encodeToString(data)) + assertEquals(data, Json.decodeFromString<Data2>("""{"a":1,"b":2}""")) + + // Serialize with external serializer for Data class + assertEquals("""{"a":1,"b":2}""", default.encodeToString(ExtDataSerializer2, data)) + assertEquals(data, Json.decodeFromString(ExtDataSerializer2, """{"a":1,"b":2}""")) + } + + enum class TintEnum { LIGHT, DARK } + + @Serializable + data class Data3( + val a: String, + val b: List<Int>, + val c: Map<String, TintEnum> + ) + + // Serialize with external serializer for Data class + @Serializer(forClass = Data3::class) + object ExtDataSerializer3 + + @Test + fun testNestedValues() { + val data = Data3("Str", listOf(1, 2), mapOf("lt" to TintEnum.LIGHT, "dk" to TintEnum.DARK)) + // Serialize with internal serializer for Data class + val expected = """{"a":"Str","b":[1,2],"c":{"lt":"LIGHT","dk":"DARK"}}""" + assertEquals(expected, default.encodeToString(data)) + assertEquals(data, Json.decodeFromString<Data3>(expected)) + assertEquals(expected, default.encodeToString(ExtDataSerializer3, data)) + assertEquals(data, Json.decodeFromString(ExtDataSerializer3, expected)) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializeJavaClassTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializeJavaClassTest.kt new file mode 100644 index 00000000..a2f900d0 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializeJavaClassTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.json.Json +import org.junit.Test +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import kotlin.test.assertEquals + +object DateSerializer : KSerializer<Date> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.util.Date", PrimitiveKind.STRING) + + // Consider wrapping in ThreadLocal if serialization may happen in multiple threads + private val df: DateFormat = SimpleDateFormat("dd/MM/yyyy HH:mm:ss.SSS").apply { + timeZone = TimeZone.getTimeZone("GMT+2") + } + + override fun serialize(encoder: Encoder, value: Date) { + encoder.encodeString(df.format(value)) + } + + override fun deserialize(decoder: Decoder): Date { + return df.parse(decoder.decodeString()) + } +} + +@Serializable +data class ClassWithDate(@Serializable(with = DateSerializer::class) val date: Date) + +class SerializeJavaClassTest { + @Test + fun serializeToStringAndRestore() { + // Thursday, 4 October 2018 09:00:00 GMT+02:00 — KotlinConf 2018 Keynote + val date = ClassWithDate(Date(1538636400000L)) + val s = Json.encodeToString(date) + assertEquals("""{"date":"04/10/2018 09:00:00.000"}""", s) + val date2 = Json.decodeFromString(ClassWithDate.serializer(), s) + assertEquals(date, date2) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializerByTypeCacheTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializerByTypeCacheTest.kt new file mode 100644 index 00000000..b8dd35d1 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializerByTypeCacheTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization + +import kotlinx.serialization.json.Json +import java.net.URLClassLoader +import kotlin.reflect.* +import kotlin.test.* + +class SerializerByTypeCacheTest { + + @Serializable + class Holder(val i: Int) + + @Suppress("UNCHECKED_CAST") + @Test + fun testCaching() { + val typeOfKType = typeOf<Holder>() + val parameterKType = typeOf<List<Holder>>().arguments[0].type!! + assertSame(serializer(), serializer<Holder>()) + assertSame(serializer(typeOfKType), serializer(typeOfKType)) + assertSame(serializer(parameterKType), serializer(parameterKType)) + assertSame(serializer(), serializer(typeOfKType) as KSerializer<Holder>) + assertSame(serializer(parameterKType) as KSerializer<Holder>, serializer(typeOfKType) as KSerializer<Holder>) + } + + /** + * Checking the case when a parameterized type is loaded in different parallel [ClassLoader]s. + * + * If the main type is loaded by a common parent [ClassLoader] (for example, a bootstrap for [List]), + * and the element class is loaded by different loaders, then some implementations of the [KType] (e.g. `KTypeImpl` from reflection) may not see the difference between them. + * + * As a result, a serializer for another loader will be returned from the cache, and it will generate instances, when working with which we will get an [ClassCastException]. + * + * The test checks the correctness of the cache for such cases - that different serializers for different loaders will be returned. + * + * [see](https://youtrack.jetbrains.com/issue/KT-54523). + */ + @Test + fun testDifferentClassLoaders() { + val elementKType1 = SimpleKType(loadClass().kotlin) + val elementKType2 = SimpleKType(loadClass().kotlin) + + // Java class must be same (same name) + assertEquals(elementKType1.classifier.java.canonicalName, elementKType2.classifier.java.canonicalName) + // class loaders must be different + assertNotSame(elementKType1.classifier.java.classLoader, elementKType2.classifier.java.classLoader) + // due to the incorrect definition of the `equals`, KType-s are equal + assertEquals(elementKType1, elementKType2) + + // create parametrized type `List<Foo>` + val kType1 = SingleParametrizedKType(List::class, elementKType1) + val kType2 = SingleParametrizedKType(List::class, elementKType2) + + val serializer1 = serializer(kType1) + val serializer2 = serializer(kType2) + + // when taking a serializers from cache, we must distinguish between KType-s, despite the fact that they are equivalent + assertNotSame(serializer1, serializer2) + + // serializers must work correctly + Json.decodeFromString(serializer1, "[{\"i\":1}]") + Json.decodeFromString(serializer2, "[{\"i\":1}]") + } + + /** + * Load class `example.Foo` via new class loader. Compiled class-file located in the resources. + */ + private fun loadClass(): Class<*> { + val classesUrl = this::class.java.classLoader.getResource("class_loaders/classes/") + val loader1 = URLClassLoader(arrayOf(classesUrl), this::class.java.classLoader) + return loader1.loadClass("example.Foo") + } + + private class SimpleKType(override val classifier: KClass<*>): KType { + override val annotations: List<Annotation> = emptyList() + override val arguments: List<KTypeProjection> = emptyList() + + override val isMarkedNullable: Boolean = false + + override fun equals(other: Any?): Boolean { + if (other !is SimpleKType) return false + return classifier.java.canonicalName == other.classifier.java.canonicalName + } + + override fun hashCode(): Int { + return classifier.java.canonicalName.hashCode() + } + } + + + private class SingleParametrizedKType(override val classifier: KClass<*>, val parameterType: KType): KType { + override val annotations: List<Annotation> = emptyList() + + override val arguments: List<KTypeProjection> = listOf(KTypeProjection(KVariance.INVARIANT, parameterType)) + + override val isMarkedNullable: Boolean = false + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SingleParametrizedKType + + if (classifier != other.classifier) return false + if (parameterType != other.parameterType) return false + + return true + } + + override fun hashCode(): Int { + var result = classifier.hashCode() + result = 31 * result + parameterType.hashCode() + return result + } + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializerForNullableJavaTypeTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializerForNullableJavaTypeTest.kt new file mode 100644 index 00000000..ebed6f37 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/SerializerForNullableJavaTypeTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.json.* +import org.junit.Test +import java.util.* +import kotlin.test.* + +class SerializerForNullableJavaTypeTest { + + // User-reported generic serialization + class DateSerializer : KSerializer<Date?> { + + override val descriptor = PrimitiveSerialDescriptor("LocalTime?", PrimitiveKind.LONG).nullable + + override fun deserialize(decoder: Decoder): Date? = when (val seconds = decoder.decodeLong()) { + -1L -> null + else -> Date(seconds) + } + + override fun serialize(encoder: Encoder, value: Date?) { + when (value) { + null -> encoder.encodeLong(-1L) //this line is never reached despite that nulls exist in serialized lists + else -> encoder.encodeLong(value.toInstant().toEpochMilli()) + } + } + } + + @Serializable + private data class ListWrapper(val times: List<@Serializable(with = DateSerializer::class) Date?>) + + @Test + fun testMixedList() { + val data = ListWrapper(listOf(Date(42), null)) + val str = Json.encodeToString(data) + assertEquals("""{"times":[42,-1]}""", str) + assertEquals(data, Json.decodeFromString(str)) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt new file mode 100644 index 00000000..dcf3e959 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/StacktraceRecoveryTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization + +import kotlinx.coroutines.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.json.* +import kotlinx.serialization.json.internal.* +import kotlinx.serialization.modules.* +import kotlin.test.* + +class StacktraceRecoveryTest { + @Serializable + private class Data(val s: String) + + private class BadDecoder : AbstractDecoder() { + override val serializersModule: SerializersModule = EmptySerializersModule() + override fun decodeElementIndex(descriptor: SerialDescriptor): Int = 42 + } + + @Test + fun testJsonDecodingException() = checkRecovered("JsonDecodingException") { + Json.decodeFromString<String>("42") + } + + @Test + fun testJsonEncodingException() = checkRecovered("JsonEncodingException") { + Json.encodeToString(Double.NaN) + } + + @Test + // checks simple name because UFE is internal class + fun testUnknownFieldException() = checkRecovered("UnknownFieldException") { + val serializer = Data.serializer() + serializer.deserialize(BadDecoder()) + } + + private fun checkRecovered(exceptionClassSimpleName: String, block: () -> Unit) = runBlocking { + val result = runCatching { + callBlockWithRecovery(block) + } + assertTrue(result.isFailure, "Block should have failed") + val e = result.exceptionOrNull()!! + assertEquals(exceptionClassSimpleName, e::class.simpleName!!) + val cause = e.cause + assertNotNull(cause, "Exception should have cause: $e") + assertEquals(e.message, cause.message) + assertEquals(exceptionClassSimpleName, e::class.simpleName!!) + } + + // KLUDGE: A separate function with state-machine to ensure coroutine DebugMetadata is generated. See KT-41789 + private suspend fun callBlockWithRecovery(block: () -> Unit) { + yield() + // use withContext to perform switch between coroutines and thus trigger exception recovery machinery + withContext(NonCancellable) { + block() + } + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/ContextualSerializationOnFileTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/ContextualSerializationOnFileTest.kt new file mode 100644 index 00000000..a190a483 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/ContextualSerializationOnFileTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +// TODO: Move to common tests after https://youtrack.jetbrains.com/issue/KT-28927 is fixed + +@file:UseContextualSerialization(Int::class, IntHolder::class) + +package kotlinx.serialization.features + +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlinx.serialization.modules.* +import kotlin.test.* + +@Serializable +data class Carrier3( + val a: IntHolder, + val i: Int, + val nullable: Int?, + val nullableIntHolder: IntHolder?, + val nullableIntList: List<Int?> = emptyList(), + val nullableIntHolderNullableList: List<IntHolder?>? = null +) + +class ContextualSerializationOnFileTest { + val module = SerializersModule { + contextual(DividingIntSerializer) + contextual(MultiplyingIntHolderSerializer) + } + val json = Json { serializersModule = module; encodeDefaults = true } + + @Test + fun testOnFile() { + val str = json.encodeToString(Carrier3.serializer(), Carrier3(IntHolder(42), 8, 8, IntHolder(42))) + assertEquals( + """{"a":84,"i":4,"nullable":4,"nullableIntHolder":84,"nullableIntList":[],"nullableIntHolderNullableList":null}""", + str + ) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/InternalInheritanceTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/InternalInheritanceTest.kt new file mode 100644 index 00000000..6a1f722e --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/InternalInheritanceTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2017-2019 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.json.* +import org.junit.* +import org.junit.Assert.* + +class InternalInheritanceTest : JsonTestBase() { + @Serializable + open class A(val parent: Int) { + private val rootOptional = "rootOptional" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is A) return false + + if (parent != other.parent) return false + if (rootOptional != other.rootOptional) return false + + return true + } + } + + @Serializable + open class B(val parent2: Int, @Transient val transientDerived: String = "X", val derived: String) : A(parent2) { + protected val bodyDerived = "body" + } + + @Serializable + class C(val parent3: Int) : B(parent3, derived = "derived") { + val lastDerived = "optional" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as C + + if (!super.equals(other)) return false + if (parent3 != other.parent3) return false + if (lastDerived != other.lastDerived) return false + if (parent2 != other.parent2) return false + if (transientDerived != other.transientDerived) return false + if (derived != other.derived) return false + if (bodyDerived != other.bodyDerived) return false + + return true + } + } + + @Test + fun testEncodeToString() { + assertEquals( + """{"parent":42,"rootOptional":"rootOptional","parent2":42,"derived":"derived",""" + + """"bodyDerived":"body","parent3":42,"lastDerived":"optional"}""", + default.encodeToString(C(42)) + ) + assertEquals( + """{"parent":13,"rootOptional":"rootOptional","parent2":13,"derived":"bbb","bodyDerived":"body"}""", + default.encodeToString(B(13, derived = "bbb")) + ) + } + + @Test + fun testParse() { + assertEquals( + C(42), + default.decodeFromString<C>( + """{"parent":42,"rootOptional":"rootOptional","parent2":42,""" + + """"derived":"derived","bodyDerived":"body","parent3":42,"lastDerived":"optional"}""" + ) + ) + assertEquals( + C(43), + default.decodeFromString<C>("""{"parent":43,"rootOptional":"rootOptional","parent2":43,"derived":"derived",""" + + """"bodyDerived":"body","parent3":43,"lastDerived":"optional"}""") + ) + } + + @Test + fun testParseOptionals() { + assertEquals( + B(100, derived = "wowstring"), + default.decodeFromString<B>("""{"parent":100,"rootOptional":"rootOptional","parent2":100,"derived":"wowstring","bodyDerived":"body"}""") + ) + assertEquals( + C(44), + default.decodeFromString<C>("""{"parent":44, "parent2":44,"derived":"derived","bodyDerived":"body","parent3":44}""") + ) + assertEquals( + B(101, derived = "wowstring"), + default.decodeFromString<B>("""{"parent":101,"parent2":101,"derived":"wowstring","bodyDerived":"body"}""") + ) + assertEquals( + A(77), + default.decodeFromString<A>("""{"parent":77,"rootOptional":"rootOptional"}""") + ) + assertEquals( + A(78), + default.decodeFromString<A>("""{"parent":78}""") + ) + } + + @Test(expected = SerializationException::class) + fun testThrowTransient() { + Json.decodeFromString<B>("""{"parent":100,"rootOptional":"rootOptional","transientDerived":"X",""" + + """"parent2":100,"derived":"wowstring","bodyDerived":"body"}""") + } + + @Test(expected = SerializationException::class) + fun testThrowMissingField() { + default.decodeFromString<C>("""{"parent":42,"rootOptional":"rootOptional","derived":"derived",""" + + """"bodyDerived":"body","parent3":42,"lastDerived":"optional"}""") + } + + @Test + fun testSerializeAsParent() { + val obj1: A = B(77, derived = "derived") + val obj2: A = C(77) + assertEquals("""{"parent":77,"rootOptional":"rootOptional"}""", default.encodeToString(obj1)) + assertEquals("""{"parent":77,"rootOptional":"rootOptional"}""", default.encodeToString(obj2)) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonJvmStreamsTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonJvmStreamsTest.kt new file mode 100644 index 00000000..7019edae --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonJvmStreamsTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package kotlinx.serialization.features + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.* +import kotlinx.serialization.json.internal.BATCH_SIZE +import kotlinx.serialization.modules.* +import kotlinx.serialization.test.* +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class JsonJvmStreamsTest { + private val strLen = BATCH_SIZE * 2 + 42 + + @Test + fun testParsesStringsLongerThanBuffer() { + val str = "a".repeat(strLen) + val input = """{"data":"$str"}""" + assertEquals(input, Json.encodeViaStream(StringData.serializer(), StringData(str))) + assertEquals(str, Json.decodeViaStream(StringData.serializer(), input).data) + assertEquals(str, Json.decodeViaStream(String.serializer(), "\"$str\"")) + } + + @Test + fun testSkipsWhitespacesLongerThanBuffer() { + val str = "a".repeat(strLen) + val ws = " ".repeat(strLen) + val input = """{"data":$ws"$str"}""" + assertEquals("""{"data":"$str"}""", Json.encodeViaStream(StringData.serializer(), StringData(str))) + assertEquals(str, Json.decodeViaStream(StringData.serializer(), input).data) + } + + @Test + fun testHandlesEscapesLongerThanBuffer() { + val str = "\\t".repeat(strLen) + val expected = "\t".repeat(strLen) + val input = """{"data":"$str"}""" + assertEquals(input, Json.encodeViaStream(StringData.serializer(), StringData(expected))) + assertEquals(expected, Json.decodeViaStream(StringData.serializer(), input).data) + } + + @Test + fun testHandlesLongLenientStrings() { + val str = "a".repeat(strLen) + val input = """{"data":$str}""" + val json = Json { isLenient = true } + assertEquals(str, json.decodeViaStream(StringData.serializer(), input).data) + assertEquals(str, json.decodeViaStream(String.serializer(), str)) + } + + @Test + fun testThrowsCorrectExceptionOnEof() { + assertFailsWith<SerializationException> { + Json.decodeViaStream(StringData.serializer(), """{"data":""") + } + assertFailsWith<SerializationException> { + Json.decodeViaStream(StringData.serializer(), "") + } + assertFailsWith<SerializationException> { + Json.decodeViaStream(String.serializer(), "\"") + } + } + + @Test + fun testRandomEscapeSequences() { + repeat(1000) { + val s = generateRandomUnicodeString(strLen) + try { + val serializer = String.serializer() + val b = ByteArrayOutputStream() + Json.encodeToStream(serializer, s, b) + val restored = Json.decodeFromStream(serializer, ByteArrayInputStream(b.toByteArray())) + assertEquals(s, restored) + } catch (e: Throwable) { + // Not assertion error to preserve cause + throw IllegalStateException("Unexpectedly failed test, cause string: $s", e) + } + } + } + + interface Poly + + @Serializable + @SerialName("Impl") + data class Impl(val str: String) : Poly + + @Test + fun testPolymorphismWhenCrossingBatchSizeNonLeadingKey() { + val json = Json { + serializersModule = SerializersModule { + polymorphic(Poly::class) { + subclass(Impl::class, Impl.serializer()) + } + } + } + + val longString = "a".repeat(BATCH_SIZE - 5) + val string = """{"str":"$longString", "type":"Impl"}""" + val golden = Impl(longString) + + val deserialized = json.decodeViaStream(serializer<Poly>(), string) + assertEquals(golden, deserialized as Impl) + } + + @Test + fun testPolymorphismWhenCrossingBatchSize() { + val json = Json { + serializersModule = SerializersModule { + polymorphic(Poly::class) { + subclass(Impl::class, Impl.serializer()) + } + } + } + + val aLotOfWhiteSpaces = " ".repeat(BATCH_SIZE - 5) + val string = """{$aLotOfWhiteSpaces"type":"Impl", "str":"value"}""" + val golden = Impl("value") + + val deserialized = json.decodeViaStream(serializer<Poly>(), string) + assertEquals(golden, deserialized as Impl) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonLazySequenceTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonLazySequenceTest.kt new file mode 100644 index 00000000..23887660 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonLazySequenceTest.kt @@ -0,0 +1,197 @@ +/* + * 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.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.* +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.features.sealed.SealedChild +import kotlinx.serialization.features.sealed.SealedParent +import kotlinx.serialization.json.* +import kotlinx.serialization.test.assertFailsWithMessage +import kotlinx.serialization.test.assertFailsWithSerial +import org.junit.Test +import java.io.* +import kotlin.test.* + +class JsonLazySequenceTest { + val json = Json + + private suspend inline fun <reified T> Flow<T>.writeToStream(os: OutputStream) { + collect { + json.encodeToStream(it, os) + } + } + + private suspend inline fun <reified T> Json.readFromStream(iss: InputStream): Flow<T> = flow { + val serial = serializer<T>() + val iter = iterateOverStream(iss, serial) + while (iter.hasNext()) { + emit(iter.next()) + } + }.flowOn(Dispatchers.IO) + + private val inputStringWsSeparated = """{"data":"a"}{"data":"b"}{"data":"c"}""" + private val inputStringWrapped = """[{"data":"a"},{"data":"b"},{"data":"c"}]""" + private val inputList = listOf(StringData("a"), StringData("b"), StringData("c")) + + @Test + fun testEncodeSeveralItems() { + val items = inputList + val os = ByteArrayOutputStream() + runBlocking { + val f = flow<StringData> { items.forEach { emit(it) } } + f.writeToStream(os) + } + + assertEquals(inputStringWsSeparated, os.toString(Charsets.UTF_8.name())) + } + + @Test + fun testDecodeSeveralItems() { + val ins = ByteArrayInputStream(inputStringWsSeparated.encodeToByteArray()) + assertFailsWithMessage<SerializationException>("EOF") { + json.decodeFromStream<StringData>(ins) + } + } + + private inline fun <reified T> Iterator<T>.assertNext(expected: T) { + assertTrue(hasNext()) + assertEquals(expected, next()) + } + + private fun <T> Json.iterateOverStream(stream: InputStream, deserializer: DeserializationStrategy<T>): Iterator<T> = + decodeToSequence(stream, deserializer).iterator() + + private fun withInputs(vararg inputs: String = arrayOf(inputStringWsSeparated, inputStringWrapped), block: (InputStream) -> Unit) { + for (input in inputs) { + val res = runCatching { block(input.asInputStream()) } + if (res.isFailure) throw AssertionError("Failed test with input $input", res.exceptionOrNull()) + } + } + + private fun String.asInputStream() = ByteArrayInputStream(this.encodeToByteArray()) + + @Test + fun testIterateSeveralItems() = withInputs { ins -> + val iter = json.iterateOverStream(ins, StringData.serializer()) + iter.assertNext(StringData("a")) + iter.assertNext(StringData("b")) + iter.assertNext(StringData("c")) + assertFalse(iter.hasNext()) + assertFalse(iter.hasNext()) // Subsequent calls to .hasNext() should not throw EOF or anything + assertFailsWithMessage<SerializationException>("EOF") { + iter.next() + } + } + + @Test + fun testDecodeToSequence() = withInputs { ins -> + val sequence = json.decodeToSequence(ins, StringData.serializer()) + assertEquals(inputList, sequence.toList(), "For input $inputStringWsSeparated") + assertFailsWith<IllegalStateException> { sequence.toList() } // assert constrained once + } + + @Test + fun testDecodeAsFlow() = withInputs { ins -> + val list = runBlocking { + buildList { json.readFromStream<StringData>(ins).toCollection(this) } + } + assertEquals(inputList, list) + } + + @Test + fun testItemsSeparatedByWs() { + val input = "{\"data\":\"a\"} {\"data\":\"b\"}\n\t{\"data\":\"c\"}" + val ins = ByteArrayInputStream(input.encodeToByteArray()) + assertEquals(inputList, json.decodeToSequence(ins, StringData.serializer()).toList()) + } + + @Test + fun testJsonElement() { + val list = listOf<JsonElement>( + buildJsonObject { put("foo", "bar") }, + buildJsonObject { put("foo", "baz") }, + JsonPrimitive(10), + JsonPrimitive("abacaba"), + buildJsonObject { put("foo", "qux") } + ) + val inputWs = """${list[0]} ${list[1]} ${list[2]} ${list[3]} ${list[4]}""" + val decodedWs = json.decodeToSequence<JsonElement>(inputWs.asInputStream()).toList() + assertEquals(list, decodedWs, "Failed whitespace-separated test") + val inputArray = """[${list[0]}, ${list[1]},${list[2]} , ${list[3]} ,${list[4]}]""" + val decodedArrayWrap = json.decodeToSequence<JsonElement>(inputArray.asInputStream()).toList() + assertEquals(list, decodedArrayWrap, "Failed array-wrapped test") + } + + + @Test + fun testSealedClasses() { + val input = """{"type":"first child","i":1,"j":10} {"type":"first child","i":1,"j":11}""" + val iter = json.iterateOverStream(input.asInputStream(), SealedParent.serializer()) + iter.assertNext(SealedChild(10)) + iter.assertNext(SealedChild(11)) + } + + @Test + fun testMalformedArray() { + val input1 = """[1, 2, 3""" + val input2 = """[1, 2, 3]qwert""" + val input3 = """[1,2 3]""" + withInputs(input1, input2, input3) { + assertFailsWithSerial("JsonDecodingException") { + json.decodeToSequence(it, Int.serializer()).toList() + } + } + } + + @Test + fun testMultilineArrays() { + val input = "[1,2,3]\n[4,5,6]\n[7,8,9]" + assertFailsWithSerial("JsonDecodingException") { + json.decodeToSequence<List<Int>>(input.asInputStream(), DecodeSequenceMode.AUTO_DETECT).toList() + } + assertFailsWithSerial("JsonDecodingException") { + json.decodeToSequence<Int>(input.asInputStream(), DecodeSequenceMode.AUTO_DETECT).toList() + } + assertFailsWithSerial("JsonDecodingException") { // we do not merge lists + json.decodeToSequence<Int>(input.asInputStream(), DecodeSequenceMode.ARRAY_WRAPPED).toList() + } + val parsed = json.decodeToSequence<List<Int>>(input.asInputStream(), DecodeSequenceMode.WHITESPACE_SEPARATED).toList() + val expected = listOf(listOf(1,2,3), listOf(4,5,6), listOf(7,8,9)) + assertEquals(expected, parsed) + } + + @Test + fun testStrictArrayCheck() { + assertFailsWithSerial("JsonDecodingException") { + json.decodeToSequence<StringData>(inputStringWsSeparated.asInputStream(), DecodeSequenceMode.ARRAY_WRAPPED) + } + } + + @Test + fun testPaddedWs() { + val paddedWs = " $inputStringWsSeparated " + assertEquals(inputList, json.decodeToSequence(paddedWs.asInputStream(), StringData.serializer()).toList()) + assertEquals(inputList, json.decodeToSequence(paddedWs.asInputStream(), StringData.serializer(), DecodeSequenceMode.WHITESPACE_SEPARATED).toList()) + } + + @Test + fun testPaddedArray() { + val paddedWs = " $inputStringWrapped " + assertEquals(inputList, json.decodeToSequence(paddedWs.asInputStream(), StringData.serializer()).toList()) + assertEquals(inputList, json.decodeToSequence(paddedWs.asInputStream(), StringData.serializer(), DecodeSequenceMode.ARRAY_WRAPPED).toList()) + } + + @Test + fun testToIteratorAndBack() = withInputs { ins -> + val iterator = Json.decodeToSequence(ins, StringData.serializer()).iterator() + val values = iterator.asSequence().take(3).toList() + assertEquals(inputList, values) + assertFalse(iterator.hasNext()) + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonSequencePathTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonSequencePathTest.kt new file mode 100644 index 00000000..554deab1 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/JsonSequencePathTest.kt @@ -0,0 +1,33 @@ +/* + * 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.Serializable +import kotlinx.serialization.json.* +import kotlinx.serialization.test.assertFailsWithMessage +import org.junit.Test +import java.io.* + +class JsonSequencePathTest { + + @Serializable + class NestedData(val s: String) + + @Serializable + class Data(val data: NestedData) + + @Test + fun testFailure() { + val source = """{"data":{"s":"value"}}{"data":{"s":42}}{notevenreached}""".toStream() + val iterator = Json.decodeToSequence<Data>(source).iterator() + iterator.next() // Ignore + assertFailsWithMessage<SerializationException>( + "Expected quotation mark '\"', but had '4' instead at path: \$.data.s" + ) { iterator.next() } + } + + private fun String.toStream() = ByteArrayInputStream(encodeToByteArray()) +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt new file mode 100644 index 00000000..a600b9d7 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/features/SerializerByTypeTest.kt @@ -0,0 +1,292 @@ +/* + * 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 org.junit.Test +import java.lang.reflect.* +import kotlin.reflect.* +import kotlin.test.* +import kotlin.time.* + +class SerializerByTypeTest { + + private val json = Json + + @Serializable + data class Box<out T>(val a: T) + + @Serializable + data class Data(val l: List<String>, val b: Box<Int>) + + @Serializable(WithCustomDefault.Companion::class) + data class WithCustomDefault(val n: Int) { + + companion object: KSerializer<WithCustomDefault> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("WithCustomDefault", PrimitiveKind.INT) + override fun serialize(encoder: Encoder, value: WithCustomDefault) = encoder.encodeInt(value.n) + override fun deserialize(decoder: Decoder) = WithCustomDefault(decoder.decodeInt()) + } + } + + object IntBoxToken : ParameterizedType { + override fun getRawType() = Box::class.java + override fun getOwnerType() = null + override fun getActualTypeArguments(): Array<Type> = arrayOf(Int::class.java) + } + + @Serializable + object SerializableObject + + @Serializable + data class WithNamedCompanion(val a: Int) { + companion object Named + } + + @Test + fun testGenericParameter() { + val b = Box(42) + assertSerializedWithType(IntBoxToken, """{"a":42}""", b) + } + + @Test + fun testNestedGenericParameter() { + val b = Box(Box(239)) + assertSerializedWithType(typeTokenOf<Box<Box<Int>>>(), """{"a":{"a":239}}""", b) + } + + @Test + fun testArray() { + val myArr = arrayOf("a", "b", "c") + val token = myArr::class.java + assertSerializedWithType(token, """["a","b","c"]""", myArr) + } + + @Test + fun testList() { + val myArr = listOf("a", "b", "c") + val token = object : ParameterizedType { + override fun getRawType(): Type = List::class.java + override fun getOwnerType(): Type? = null + override fun getActualTypeArguments(): Array<Type> = arrayOf(String::class.java) + } + assertSerializedWithType(token, """["a","b","c"]""", myArr) + } + + @Test + fun testListAsCollection() { + val myArr: Collection<String> = listOf("a", "b", "c") + val token = object : ParameterizedType { + override fun getRawType(): Type = Collection::class.java + override fun getOwnerType(): Type? = null + override fun getActualTypeArguments(): Array<Type> = arrayOf(String::class.java) + } + assertSerializedWithType(token, """["a","b","c"]""", myArr) + } + + + @Test + fun testReifiedArrayResolving() { + val myArr = arrayOf("a", "b", "c") + val token = typeTokenOf<Array<String>>() + assertSerializedWithType(token, """["a","b","c"]""", myArr) + } + + @Test + fun testPrimitiveArrayResolving() { + val myArr = intArrayOf(1, 2, 3) + val token = IntArray::class.java + val name = serializer(token).descriptor.serialName + assertTrue(name.contains("IntArray")) + assertSerializedWithType(token, """[1,2,3]""", myArr) + } + + @Test + fun testReifiedListResolving() { + val myList = listOf("a", "b", "c") + val token = typeTokenOf<List<String>>() + assertSerializedWithType(token, """["a","b","c"]""", myList) + } + + @Test + fun testReifiedSetResolving() { + val mySet = setOf("a", "b", "c", "c") + val token = typeTokenOf<Set<String>>() + assertSerializedWithType(token, """["a","b","c"]""", mySet) + } + + @Test + fun testReifiedMapResolving() { + val myMap = mapOf("a" to Data(listOf("c"), Box(6))) + val token = typeTokenOf<Map<String, Data>>() + assertSerializedWithType(token, """{"a":{"l":["c"],"b":{"a":6}}}""",myMap) + } + + @Test + fun testNestedListResolving() { + val myList = listOf(listOf(listOf(1, 2, 3)), listOf()) + val token = typeTokenOf<List<List<List<Int>>>>() + assertSerializedWithType(token, "[[[1,2,3]],[]]", myList) + } + + @Test + fun testNestedArrayResolving() { + val myList = arrayOf(arrayOf(arrayOf(1, 2, 3)), arrayOf()) + val token = typeTokenOf<Array<Array<Array<Int>>>>() + assertSerializedWithType(token, "[[[1,2,3]],[]]", myList) + } + + @Test + fun testNestedMixedResolving() { + val myList = arrayOf(listOf(arrayOf(1, 2, 3)), listOf()) + val token = typeTokenOf<Array<List<Array<Int>>>>() + assertSerializedWithType(token, "[[[1,2,3]],[]]", myList) + } + + @Test + fun testPair() { + val myPair = "42" to 42 + val token = typeTokenOf<Pair<String, Int>>() + assertSerializedWithType(token, """{"first":"42","second":42}""", myPair) + } + + @Test + fun testTriple() { + val myTriple = Triple("1", 2, Box(42)) + val token = typeTokenOf<Triple<String, Int, Box<Int>>>() + assertSerializedWithType(token, """{"first":"1","second":2,"third":{"a":42}}""", myTriple) + } + + @Test + fun testGenericInHolder() { + val b = Data(listOf("a", "b", "c"), Box(42)) + assertSerializedWithType(Data::class.java,"""{"l":["a","b","c"],"b":{"a":42}}""", b ) + } + + @Test + fun testOverriddenSerializer() { + val foo = json.decodeFromString<WithCustomDefault>("9") + assertEquals(9, foo.n) + } + + @Test + fun testNamedCompanion() { + val namedCompanion = WithNamedCompanion(1) + assertSerializedWithType(WithNamedCompanion::class.java, """{"a":1}""", namedCompanion) + } + + @Test + fun testPrimitive() { + val token = typeTokenOf<Int>() + val serial = serializer(token) + assertSame(Int.serializer() as KSerializer<*>, serial) + } + + @Test + fun testObject() { + val token = typeTokenOf<SerializableObject>() + val serial = serializer(token) + assertEquals(SerializableObject.serializer().descriptor, serial.descriptor) + } + + @Suppress("UNCHECKED_CAST") + private fun <T> assertSerializedWithType( + token: Type, + expected: String, + value: T, + ) { + val serial = serializer(token) as KSerializer<T> + assertEquals(expected, json.encodeToString(serial, value)) + val serial2 = requireNotNull(serializerOrNull(token)) { "Expected serializer to be found" } + assertEquals(expected, json.encodeToString(serial2 as KSerializer<T>, value)) + } + + class IntBox(val i: Int) + + object CustomIntSerializer : KSerializer<IntBox> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CIS", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: IntBox) { + encoder.encodeInt(42) + } + + override fun deserialize(decoder: Decoder): IntBox { + TODO() + } + } + + @Test + fun testContextualLookup() { + val module = SerializersModule { contextual(CustomIntSerializer) } + val serializer = module.serializer(typeTokenOf<List<List<IntBox>>>()) + assertEquals("[[42]]", Json.encodeToString(serializer, listOf(listOf(IntBox(1))))) + } + + @Test + fun testCompiledWinsOverContextual() { + val contextual = object : KSerializer<Int> { + override val descriptor: SerialDescriptor = Int.serializer().descriptor + + override fun serialize(encoder: Encoder, value: Int) { + fail() + } + + override fun deserialize(decoder: Decoder): Int { + fail() + } + } + val module = SerializersModule { contextual(contextual) } + val serializer = module.serializer(typeTokenOf<List<List<Int>>>()) + assertEquals("[[1]]", Json.encodeToString(serializer, listOf(listOf<Int>(1)))) + assertEquals("42", Json.encodeToString(module.serializer(typeTokenOf<Int>()), 42)) + } + + class NonSerializable + + class NonSerializableBox<T>(val boxed: T) + + @Test + fun testLookupFail() { + assertNull(serializerOrNull(typeTokenOf<NonSerializable>())) + assertNull(serializerOrNull(typeTokenOf<NonSerializableBox<String>>())) + assertNull(serializerOrNull(typeTokenOf<Box<NonSerializable>>())) + + assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") { + serializer(typeTokenOf<NonSerializable>()) + } + + assertFailsWithMessage<SerializationException>("for class 'NonSerializableBox'") { + serializer(typeTokenOf<NonSerializableBox<String>>()) + } + + assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") { + serializer(typeTokenOf<kotlinx.serialization.Box<NonSerializable>>()) + } + + assertFailsWithMessage<SerializationException>("for class 'NonSerializable'") { + serializer(typeTokenOf<Array<NonSerializable>>()) + } + } + + @OptIn(ExperimentalTime::class) + @Test + fun testSerializersAreIntrinsified() { + val direct = measureTime { + Json.encodeToString(IntData.serializer(), IntData(10)) + } + val directMs = direct.inWholeMicroseconds + val indirect = measureTime { + Json.encodeToString(IntData(10)) + } + val indirectMs = indirect.inWholeMicroseconds + if (indirectMs > directMs + (directMs / 4)) error("Direct ($directMs) and indirect ($indirectMs) times are too far apart") + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/json/GsonCompatibilityTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/GsonCompatibilityTest.kt new file mode 100644 index 00000000..99bc23f9 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/GsonCompatibilityTest.kt @@ -0,0 +1,56 @@ +package kotlinx.serialization.json + +import com.google.gson.* +import kotlinx.serialization.* +import org.junit.Test +import kotlin.test.* + +class GsonCompatibilityTest { + + @Serializable + data class Box(val d: Double, val f: Float) + + @Test + fun testNaN() { + checkCompatibility(Box(Double.NaN, 1.0f)) + checkCompatibility(Box(1.0, Float.NaN)) + checkCompatibility(Box(Double.NaN, Float.NaN)) + } + + @Test + fun testInfinity() { + checkCompatibility(Box(Double.POSITIVE_INFINITY, 1.0f)) + checkCompatibility(Box(1.0, Float.POSITIVE_INFINITY)) + checkCompatibility(Box(Double.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)) + } + + @Test + fun testNumber() { + checkCompatibility(Box(23.9, 23.9f)) + } + + private fun checkCompatibility(box: Box) { + checkCompatibility(box, Gson(), Json) + checkCompatibility(box, GsonBuilder().serializeSpecialFloatingPointValues().create(), Json { allowSpecialFloatingPointValues = true }) + } + + private fun checkCompatibility(box: Box, gson: Gson, json: Json) { + val jsonResult = resultOrNull { json.encodeToString(box) } + val gsonResult = resultOrNull { gson.toJson(box) } + assertEquals(gsonResult, jsonResult) + + if (jsonResult != null && gsonResult != null) { + val jsonDeserialized: Box = json.decodeFromString(jsonResult) + val gsonDeserialized: Box = gson.fromJson(gsonResult, Box::class.java) + assertEquals(gsonDeserialized, jsonDeserialized) + } + } + + private fun resultOrNull(function: () -> String): String? { + return try { + function() + } catch (t: Throwable) { + null + } + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/json/JsonChunkedBase64DecoderTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/JsonChunkedBase64DecoderTest.kt new file mode 100644 index 00000000..35dc16fc --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/JsonChunkedBase64DecoderTest.kt @@ -0,0 +1,85 @@ +package kotlinx.serialization.json + +import kotlinx.serialization.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.test.assertFailsWithMessage +import org.junit.Test +import java.io.* +import java.util.* +import kotlin.random.Random +import kotlin.test.* + + +@Serializable(with = LargeBase64StringSerializer::class) +data class LargeBinaryData(val binaryData: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LargeBinaryData + + if (!binaryData.contentEquals(other.binaryData)) return false + + return true + } + + override fun hashCode(): Int { + return binaryData.contentHashCode() + } +} + +@Serializable +data class ClassWithBinaryDataField(val binaryField: LargeBinaryData) + +object LargeBase64StringSerializer : KSerializer<LargeBinaryData> { + private val b64Decoder: Base64.Decoder = Base64.getDecoder() + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LargeStringContent", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): LargeBinaryData { + require(decoder is ChunkedDecoder) { "Only chunked decoder supported" } + + var reminder = "" + val decodedBytes = ByteArrayOutputStream().use { bos -> + decoder.decodeStringChunked { + val actualChunk = reminder + it + val reminderLength = actualChunk.length % 4 + val alignedLength = actualChunk.length - reminderLength + val alignedChunk = actualChunk.take(alignedLength) + reminder = actualChunk.takeLast(reminderLength) + bos.write(b64Decoder.decode(alignedChunk)) + } + bos.toByteArray() + } + + return LargeBinaryData(decodedBytes) + } + + override fun serialize(encoder: Encoder, value: LargeBinaryData) { + encoder.encodeString(Base64.getEncoder().encodeToString(value.binaryData)) + } +} + +class JsonChunkedBase64DecoderTest : JsonTestBase() { + + @Test + fun decodeBase64String() { + val sourceObject = + ClassWithBinaryDataField(LargeBinaryData(Random.nextBytes(16 * 1024))) // After encoding to Base64 will be larger than 16k (JsonLexer#BATCH_SIZE) + val serializedObject = Json.encodeToString(sourceObject) + + JsonTestingMode.values().forEach { mode -> + if (mode == JsonTestingMode.TREE) { + assertFailsWithMessage<IllegalArgumentException>( + "Only chunked decoder supported", "Shouldn't decode JSON in TREE mode" + ) { + Json.decodeFromString<ClassWithBinaryDataField>(serializedObject, mode) + } + } else { + val deserializedObject = Json.decodeFromString<ClassWithBinaryDataField>(serializedObject, mode) + assertEquals(sourceObject.binaryField, deserializedObject.binaryField) + } + } + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/json/JsonConcurrentStressTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/JsonConcurrentStressTest.kt new file mode 100644 index 00000000..cdcbab79 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/JsonConcurrentStressTest.kt @@ -0,0 +1,78 @@ +package kotlinx.serialization.json + +import kotlinx.coroutines.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.* +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import kotlin.random.* +import kotlin.test.* + +// Stresses out that JSON decoded in parallel does not interfere (mostly via caching of various buffers) +class JsonConcurrentStressTest : JsonTestBase() { + private val charset = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789" + + @Test + fun testDecodeInParallelSimpleList() = doTest(100) { mode -> + val value = (1..10000).map { Random.nextDouble() } + val string = Json.encodeToString(ListSerializer(Double.serializer()), value, mode) + assertEquals(value, Json.decodeFromString(ListSerializer(Double.serializer()), string, mode)) + } + + @Serializable + data class Foo(val s: String, val f: Foo?) + + @Test + fun testDecodeInParallelListOfPojo() = doTest(1_000) { mode -> + val value = (1..100).map { + val randomString = getRandomString() + val nestedFoo = Foo("null抢\u000E鋽윝䑜厼\uF70A紲ᢨ䣠null⛾䉻嘖緝ᯧnull쎶\u0005null" + randomString, null) + Foo(getRandomString(), nestedFoo) + } + val string = Json.encodeToString(ListSerializer(Foo.serializer()), value, mode) + assertEquals(value, Json.decodeFromString(ListSerializer(Foo.serializer()), string, mode)) + } + + @Test + fun testDecodeInParallelPojo() = doTest(100_000) { mode -> + val randomString = getRandomString() + val nestedFoo = Foo("null抢\u000E鋽윝䑜厼\uF70A紲ᢨ䣠null⛾䉻嘖緝ᯧnull쎶\u0005null" + randomString, null) + val randomFoo = Foo(getRandomString(), nestedFoo) + val string = Json.encodeToString(Foo.serializer(), randomFoo, mode) + assertEquals(randomFoo, Json.decodeFromString(Foo.serializer(), string, mode)) + } + + @Test + fun testDecodeInParallelSequencePojo() = runBlocking<Unit> { + for (i in 1 until 1_000) { + launch(Dispatchers.Default) { + val values = (1..100).map { + val randomString = getRandomString() + val nestedFoo = Foo("null抢\u000E鋽윝䑜厼\uF70A紲ᢨ䣠null⛾䉻嘖緝ᯧnull쎶\u0005null" + randomString, null) + Foo(getRandomString(), nestedFoo) + } + val baos = ByteArrayOutputStream() + for (value in values) { + Json.encodeToStream(Foo.serializer(), value, baos) + } + val bais = ByteArrayInputStream(baos.toByteArray()) + assertEquals(values, Json.decodeToSequence(bais, Foo.serializer()).toList()) + } + } + } + + private fun getRandomString() = (1..Random.nextInt(0, charset.length)).map { charset[it] }.joinToString(separator = "") + + private fun doTest(iterations: Int, block: (JsonTestingMode) -> Unit) { + runBlocking<Unit> { + for (i in 1 until iterations) { + launch(Dispatchers.Default) { + parametrizedTest { + block(it) + } + } + } + } + } +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/json/MissingFieldExceptionWithPathTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/MissingFieldExceptionWithPathTest.kt new file mode 100644 index 00000000..e56f173e --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/MissingFieldExceptionWithPathTest.kt @@ -0,0 +1,40 @@ +package kotlinx.serialization.json + +import kotlinx.serialization.* +import kotlinx.serialization.json.Json.Default.decodeFromString +import org.junit.* +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.test.* + +class MissingFieldExceptionWithPathTest { + + @Test // Repro for #2212 + fun testMfeIsNotReappliedMultipleTimes() { + val inputMalformed = """{"title": "...","cast": [{}]""" + try { + Json.decodeFromString<Movie>(inputMalformed) + fail("Unreacheable state") + } catch (e: MissingFieldException) { + val fullStackTrace = e.stackTraceToString() + val i1 = fullStackTrace.toString().indexOf("at path") + val i2 = fullStackTrace.toString().lastIndexOf("at path") + assertEquals(i1, i2) + assertTrue(i1 != -1) + } + } + + @Serializable + data class Movie( + val title: String, + val cast: List<Cast>, + ) + + @Serializable + data class Cast( + val name: String + ) +} diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/json/SpecConformanceTest.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/SpecConformanceTest.kt Binary files differnew file mode 100644 index 00000000..96401f72 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/json/SpecConformanceTest.kt diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/test/CurrentPlatform.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/CurrentPlatform.kt new file mode 100644 index 00000000..91aa17f0 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/CurrentPlatform.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.test + + +public actual val currentPlatform: Platform = Platform.JVM diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/test/JsonHelpers.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/JsonHelpers.kt new file mode 100644 index 00000000..9220bbd3 --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/JsonHelpers.kt @@ -0,0 +1,20 @@ +package kotlinx.serialization.test + +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.* +import java.io.ByteArrayOutputStream + +actual fun <T> Json.encodeViaStream( + serializer: SerializationStrategy<T>, + value: T +): String { + val output = ByteArrayOutputStream() + encodeToStream(serializer, value, output) + return output.toString(Charsets.UTF_8.name()) +} + +actual fun <T> Json.decodeViaStream( + serializer: DeserializationStrategy<T>, + input: String +): T = decodeFromStream(serializer, input.byteInputStream()) diff --git a/formats/json-tests/jvmTest/src/kotlinx/serialization/test/TypeToken.kt b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/TypeToken.kt new file mode 100644 index 00000000..2b04274c --- /dev/null +++ b/formats/json-tests/jvmTest/src/kotlinx/serialization/test/TypeToken.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.test + +import java.lang.reflect.* + + +@PublishedApi +internal open class TypeBase<T> + +public inline fun <reified T> typeTokenOf(): Type { + val base = object : TypeBase<T>() {} + val superType = base::class.java.genericSuperclass!! + return (superType as ParameterizedType).actualTypeArguments.first()!! +} |