Skip to content

Commit

Permalink
Added the ability to serialize Generics.
Browse files Browse the repository at this point in the history
Remove KotlinJsonSerializerFactory.
  • Loading branch information
alexmihailov authored and ihostage committed Sep 28, 2022
1 parent ef29cc8 commit 894aaee
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 52 deletions.
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,11 @@ Supported cache implementations:
### Json-serializer that uses kotlinx-serialization (Java ✗ / Scala ✗ / Kotlin ✓)
Using `KotlinJsonSerializer` you can serialize/deserialize service responses using [kotlinx-serialization](https://github.com/Kotlin/kotlinx.serialization).
Serializable classes must be annotated with `kotlinx.serialization.Serializable`, otherwise `IllegalArgumentException` exception will be thrown when the service starts.
If you are using the `KotlinJsonSerializerFactory` in descriptor, then all service request/response classes must be annotated with `kotlinx.serialization.Serializable`.
For create `KotlinJsonSerializer`, you need to use the function `KotlinJsonSerializer.serializer`.
To set the message serializer, you need to use the extension function `withKotlinJsonSerializer` for `Descriptor`.
But this function is not intended for parameterized types, since Lagom will use one serializer for all variants.
Therefore, using `withKotlinJsonSerializer` with parameterized types will throw an `UnsupportedOperationException`.
For parameterized types(and not only), you need to use the extension functions `withRequestKotlinJsonSerializer`, `withResponseKotlinJsonSerializer` for `Descriptor.Call`.

Example:
```kotlin
Expand All @@ -244,24 +248,33 @@ data class TestData(
val field1: String,
val field2: Int
)

@Serializable
data class TestGenericData<T>(val data: T)

val json = Json { ignoreUnknownKeys = true }

interface TestService : Service {

fun testSerialization(): ServiceCall<TestData, TestData>

fun testGenericSerialization(): ServiceCall<TestGenericData<TestData>, TestGenericData<TestData>>

override fun descriptor(): Descriptor = named("test-service").withCalls(
restCall<TestData, TestData>(
Method.POST,
"/api/test/serialization",
TestService::testSerialization.javaMethod
),
) .withMessageSerializer(TestData::class.java,
KotlinJsonSerializer(
Json { ignoreUnknownKeys = true },
TestData::class.java
)
)
// .withSerializerFactory(KotlinJsonSerializerFactory ()) or use factory for all methods
restCall<TestGenericData<TestData>, TestGenericData<TestData>>(
Method.POST,
"/api/test/serialization/generic",
TestService::testGenericSerialization.javaMethod
).withRequestKotlinJsonSerializer(json)
.withResponseKotlinJsonSerializer(json),
).withKotlinJsonSerializer<TestData>(json)
}

```

## How to use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package org.taymyr.lagom.javadsl.api

import akka.util.ByteString
import akka.util.ByteStringBuilder
import com.lightbend.lagom.javadsl.api.Descriptor
import com.lightbend.lagom.javadsl.api.deser.MessageSerializer.NegotiatedDeserializer
import com.lightbend.lagom.javadsl.api.deser.MessageSerializer.NegotiatedSerializer
import com.lightbend.lagom.javadsl.api.deser.SerializerFactory
import com.lightbend.lagom.javadsl.api.deser.StrictMessageSerializer
import com.lightbend.lagom.javadsl.api.transport.MessageProtocol
import kotlinx.serialization.ExperimentalSerializationApi
Expand All @@ -14,28 +14,13 @@ import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import kotlinx.serialization.serializer
import java.io.ByteArrayInputStream
import java.lang.reflect.Type
import java.util.Optional.empty
import java.util.Optional.of

@Suppress("unused")
class KotlinJsonSerializerFactory(private val json: Json) : SerializerFactory {

/**
* Get a message json-serializer for the given type.
*
* @param type serializable type, must be annotated [kotlinx.serialization.Serializable]
* @return message json-serializer
*/
override fun <Message> messageSerializerFor(type: Type): StrictMessageSerializer<Message> =
KotlinJsonSerializer(json, type)
}

/**
* Json-serializer that uses kotlinx-serialization.
*/
class KotlinJsonSerializer<Message>(json: Json, type: Type) : StrictMessageSerializer<Message> {
private val kSerializer: KSerializer<Message> = getKSerializer(type)
class KotlinJsonSerializer<Message> @PublishedApi internal constructor(json: Json, kSerializer: KSerializer<Message>) : StrictMessageSerializer<Message> {
private val serializer = Serializer(json, kSerializer)
private val deserializer = Deserializer(json, kSerializer)

Expand All @@ -44,35 +29,82 @@ class KotlinJsonSerializer<Message>(json: Json, type: Type) : StrictMessageSeria
override fun deserializer(protocol: MessageProtocol): NegotiatedDeserializer<Message, ByteString> = deserializer

override fun serializerForResponse(acceptedMessageProtocols: List<MessageProtocol>): NegotiatedSerializer<Message, ByteString> = serializer
}

private class Serializer<MessageEntity>(
private val json: Json,
private val kSerializer: KSerializer<MessageEntity>
) : NegotiatedSerializer<MessageEntity, ByteString> {
companion object {

override fun protocol(): MessageProtocol = MessageProtocol(of("application/json"), of("utf-8"), empty())
/**
* Creates a [KotlinJsonSerializer] for the [Message] type.
*
* @param json [Json]
* @return [KotlinJsonSerializer]
*/
inline fun <reified Message> serializer(json: Json): KotlinJsonSerializer<Message> =
KotlinJsonSerializer(json, json.serializersModule.serializer())
}

@OptIn(ExperimentalSerializationApi::class)
override fun serialize(messageEntity: MessageEntity): ByteString {
val builder = ByteStringBuilder()
json.encodeToStream(kSerializer, messageEntity, builder.asOutputStream())
return builder.result()
private class Serializer<MessageEntity>(
private val json: Json,
private val kSerializer: KSerializer<MessageEntity>
) : NegotiatedSerializer<MessageEntity, ByteString> {

override fun protocol(): MessageProtocol = MessageProtocol(of("application/json"), of("utf-8"), empty())

@OptIn(ExperimentalSerializationApi::class)
override fun serialize(messageEntity: MessageEntity): ByteString {
val builder = ByteStringBuilder()
json.encodeToStream(kSerializer, messageEntity, builder.asOutputStream())
return builder.result()
}
}
}

private class Deserializer<MessageEntity>(
private val json: Json,
private val kSerializer: KSerializer<MessageEntity>
) : NegotiatedDeserializer<MessageEntity, ByteString> {
private class Deserializer<MessageEntity>(
private val json: Json,
private val kSerializer: KSerializer<MessageEntity>
) : NegotiatedDeserializer<MessageEntity, ByteString> {

@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(wire: ByteString): MessageEntity {
val inputStream = ByteArrayInputStream(wire.toArray())
return json.decodeFromStream(kSerializer, inputStream)
@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(wire: ByteString): MessageEntity {
val inputStream = ByteArrayInputStream(wire.toArray())
return json.decodeFromStream(kSerializer, inputStream)
}
}
}

@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSerializationApi::class)
private fun <Message> getKSerializer(type: Type): KSerializer<Message> = serializer(type) as KSerializer<Message>
/**
* Setting [KotlinJsonSerializer] to serialize [Request].
*
* @receiver [Descriptor.Call]
* @param json [Json]
* @return [Descriptor.Call]
*/
inline fun <reified Request, Response> Descriptor.Call<Request, Response>.withRequestKotlinJsonSerializer(json: Json): Descriptor.Call<Request, Response> =
withRequestSerializer(KotlinJsonSerializer.serializer(json))

/**
* Setting [KotlinJsonSerializer] to serialize [Response].
*
* @receiver [Descriptor.Call]
* @param json [Json]
* @return [Descriptor.Call]
*/
inline fun <reified Response, Request> Descriptor.Call<Request, Response>.withResponseKotlinJsonSerializer(json: Json): Descriptor.Call<Request, Response> =
withResponseSerializer(KotlinJsonSerializer.serializer(json))

/**
* Setting [KotlinJsonSerializer] to serialize [Message].
* When using a parameterized class, one serializer will be used for all variants.
* Therefore, this method will throw an [UnsupportedOperationException]
*
* @receiver [Descriptor]
* @param json [Json]
* @return [Descriptor]
*/
inline fun <reified Message> Descriptor.withKotlinJsonSerializer(json: Json): Descriptor = let {
if (Message::class.typeParameters.isNotEmpty()) {
throw UnsupportedOperationException(
"Parameterized types are not supported. Use withRequestKotlinJsonSerializer," +
" withResponseKotlinJsonSerializer instead of withKotlinJsonSerializer."
)
}
withMessageSerializer(Message::class.java, KotlinJsonSerializer.serializer(json))
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.taymyr.lagom.javadsl.api.KotlinJsonSerializer.Companion.serializer
import org.taymyr.lagom.javadsl.api.transport.MessageProtocols.JSON
import kotlin.random.Random

Expand All @@ -23,6 +24,9 @@ class KotlinJsonSerializerTest {
val nullableType: Double?
)

@Serializable
data class GenericTest<T>(val value: T, val temp: Int = Random.nextInt())

data class WithoutSerializable(val temp: Int = Random.nextInt())

private fun testMessage(nullableType: Double? = Random.nextDouble()) = TestMessage(
Expand All @@ -36,7 +40,7 @@ class KotlinJsonSerializerTest {

@Test
fun testSerialize() {
val serializer = KotlinJsonSerializer<TestMessage>(Json, TestMessage::class.java)
val serializer = serializer<TestMessage>(Json)

var message = testMessage()
var json = serializer.serializerForRequest().serialize(message).decodeString(Charsets.UTF_8)
Expand All @@ -47,9 +51,21 @@ class KotlinJsonSerializerTest {
assertThat(json).isEqualTo(Json.encodeToString(message))
}

@Test
fun testGenericSerialize() {
val serializer = serializer<GenericTest<TestMessage>>(Json)

val message = GenericTest(testMessage())
var json = serializer.serializerForRequest().serialize(message).decodeString(Charsets.UTF_8)
assertThat(json).isEqualTo(Json.encodeToString(message))

json = serializer.serializerForResponse(emptyList()).serialize(message).decodeString(Charsets.UTF_8)
assertThat(json).isEqualTo(Json.encodeToString(message))
}

@Test
fun testDeserialize() {
val serializer = KotlinJsonSerializer<TestMessage>(Json, TestMessage::class.java)
val serializer = serializer<TestMessage>(Json)

var expected = testMessage()
var json = Json.encodeToString(expected)
Expand All @@ -62,14 +78,28 @@ class KotlinJsonSerializerTest {
assertThat(actual).isEqualTo(expected)
}

@Test
fun testGenericDeserialize() {
val serializer = serializer<GenericTest<TestMessage>>(Json)

val expected = GenericTest(testMessage())
val json = Json.encodeToString(expected)
val actual = serializer.deserializer(JSON).deserialize(ByteString.fromString(json))
assertThat(actual).isEqualTo(expected)
}

@Test
fun testSerializeWithoutSerializable() {
assertThrows<IllegalArgumentException > {
val serializer = KotlinJsonSerializer<WithoutSerializable>(Json, WithoutSerializable::class.java)
val serializer = serializer<WithoutSerializable>(Json)
serializer.serializerForRequest().serialize(WithoutSerializable())
}
assertThrows<IllegalArgumentException > {
val serializer = KotlinJsonSerializer<WithoutSerializable>(Json, WithoutSerializable::class.java)
val serializer = serializer<GenericTest<WithoutSerializable>>(Json)
serializer.serializerForResponse(emptyList()).serialize(GenericTest(WithoutSerializable()))
}
assertThrows<IllegalArgumentException > {
val serializer = serializer<WithoutSerializable>(Json)
val json = """{ "temp": 10 }"""
serializer.deserializer(JSON).deserialize(ByteString.fromString(json))
}
Expand Down

0 comments on commit 894aaee

Please sign in to comment.