https://kotlinlang.org logo
h

Hrafn Thorvaldsson

09/30/2022, 5:13 PM
I have a question in regards to behavior differences when deserializing either raw
String
and
JsonObject
for target type classes not marked with the @Serializable annotation, and if this behavior difference is expected. Please observe the following data class:
Copy code
@Serializable
data class Test(val value: Int)
and the following manual construction of a JsonObject:
Copy code
val jsonObject = buildJsonObject {
   put("version", 1)
}
As expected when executing either
Json.decodeFromJsonElement<Test>(jsonObject)
or
Json.decodeFromString<Test>(jsonObject.toString())
the output is an instance of
Test(value=1)
. If the
@Serializable
annotation is removed from the class
Test
and a manual
KSerializer
for the class is created to handle the mapping:
Copy code
class TestSerializer: KSerializer<Test> {
	override val descriptor: SerialDescriptor
		get() = buildClassSerialDescriptor("test") {
			element<Int>("version")
		}

	override fun deserialize(decoder: Decoder): Test {
		return decoder.decodeStructure(descriptor) {
			val version = decodeIntElement(
				descriptor,
				descriptor.getElementIndex("version")
			)
			Test(version)
		}
	}
    // .... omitting serialize() method
}
then executing
Json.decodeFromJsonElement(TestSerializer(), jsonObject)
will work like before and the output is an instance of
Test(value=1)
just as before. However executing
Json.decodeFromString(TestingSerializer(), jsonObject.toString())
will result in an exception:
Copy code
Unexpected JSON token at offset 1: Unexpected symbol 'v' in numeric literal at path: $
kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 1: Unexpected symbol 'v' in numeric literal at path: $
JSON input: {"version":1}
Is this the expected behavior, and if so what is causing the string traversal to fail while the
JsonObject
is not affected?
For easier reasoning about the different cases that are mentioned above, these Kotest definitions perhaps would help:
Copy code
class DeSerializationTest: DescribeSpec( {
	describe("decode of JsonObject via annotation: Works") {
		@Serializable
		data class Test(val version: Int)

		val jsonPayload = buildJsonObject {
			put("version", 1)
		}
		val decoded = Json.decodeFromJsonElement<Test>(jsonPayload)

		decoded shouldBe Test(1)
	}
	describe("decode of String via annotation: Works") {
		@Serializable
		data class Test(val version: Int)

		val jsonPayload = buildJsonObject {
			put("version", 1)
		}
		val decoded = Json.decodeFromString<Test>(jsonPayload.toString())

		decoded shouldBe Test(1)
	}
	describe("decode of JsonObject via custom serializer: Works") {
		data class Test(val version: Int)

		class TestSerializer: KSerializer<Test> {
			override val descriptor: SerialDescriptor
				get() = buildClassSerialDescriptor("testing") {
					element<Int>("version")
				}

			override fun deserialize(decoder: Decoder): Test {
				return decoder.decodeStructure(descriptor) {
					val version = decodeIntElement(
						descriptor,
						descriptor.getElementIndex("version")
					)
					Test(version)
				}
			}

			override fun serialize(encoder: Encoder, value: Test) {
				TODO("Not yet implemented")
			}
		}

		val jsonPayload = buildJsonObject {
			put("version", 1)
		}
		val decoded = Json.decodeFromJsonElement(TestSerializer(), jsonPayload)

		decoded shouldBe Test(1)
	}
	describe("decode of String via custom serializer: Fails!") {
		data class Test(val version: Int)

		class TestSerializer: KSerializer<Test> {
			override val descriptor: SerialDescriptor
				get() = buildClassSerialDescriptor("testing") {
					element<Int>("version")
				}

			override fun deserialize(decoder: Decoder): Test {
				return decoder.decodeStructure(descriptor) {
					val version = decodeIntElement(
						descriptor,
						descriptor.getElementIndex("version")
					)
					Test(version)
				}
			}

			override fun serialize(encoder: Encoder, value: Test) {
				TODO("Not yet implemented")
			}
		}

		val jsonPayload = buildJsonObject {
			put("version", 1)
		}
		val decoded = Json.decodeFromString(TestSerializer(), jsonPayload.toString())

		decoded shouldBe Test(1)
	}
})
I'll add that this is happening using Kotlin 1.7.10, and Kotlinx Serialization 1.4.0
I have opened an issue on Github as I am starting to believe this may be a bug
2 Views