Created
December 29, 2022 10:36
-
-
Save hnaohiro/1d22a5791a7886498d1c52581e157782 to your computer and use it in GitHub Desktop.
kotlinx.serialization conditional serializer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlinx.serialization.ExperimentalSerializationApi | |
import kotlinx.serialization.KSerializer | |
import kotlinx.serialization.Serializable | |
import kotlinx.serialization.Serializer | |
import kotlinx.serialization.decodeFromString | |
import kotlinx.serialization.encodeToString | |
import kotlinx.serialization.encoding.Decoder | |
import kotlinx.serialization.encoding.Encoder | |
import kotlinx.serialization.json.Json | |
import kotlinx.serialization.json.JsonDecoder | |
import kotlinx.serialization.json.JsonEncoder | |
import kotlinx.serialization.json.JsonObject | |
import kotlinx.serialization.json.buildJsonObject | |
import kotlinx.serialization.json.encodeToJsonElement | |
import kotlinx.serialization.json.int | |
import kotlinx.serialization.json.jsonPrimitive | |
import kotlinx.serialization.json.long | |
import org.assertj.core.api.Assertions.assertThat | |
import org.junit.jupiter.api.Test | |
interface Version { | |
companion object { | |
const val value: Int = -1 | |
} | |
} | |
class Version2 : Version { | |
companion object { | |
const val value: Int = 2 | |
} | |
} | |
class Version3 : Version { | |
companion object { | |
const val value: Int = 3 | |
} | |
} | |
sealed class AddedFrom<V : Version, out T> { | |
data class Exist<V : Version, out T>(val data: T) : AddedFrom<V, T>() | |
data class NotExist<V : Version>(val unit: Unit = Unit) : AddedFrom<V, Nothing>() | |
companion object { | |
inline fun <reified V : Version, T> construct(version: Int, data: T?): AddedFrom<V, T> { | |
val addedVersion = V::class.java.getDeclaredField("value").get(null) as Int | |
return if (version >= addedVersion) { | |
Exist(requireNotNull(data)) | |
} else { | |
NotExist() | |
} | |
} | |
} | |
} | |
sealed class DeletedFrom<V : Version, out T> { | |
data class Exist<V : Version, T>(val data: T) : DeletedFrom<V, T>() | |
data class NotExist<V : Version>(val unit: Unit = Unit) : DeletedFrom<V, Nothing>() | |
companion object { | |
inline fun <reified V : Version, T> construct(version: Int, data: T?): DeletedFrom<V, T> { | |
val deletedVersion = V::class.java.getDeclaredField("value").get(null) as Int | |
return if (version < deletedVersion) { | |
Exist<V, T>(requireNotNull(data)) | |
} else { | |
NotExist() | |
} | |
} | |
} | |
} | |
sealed class ValidBetween<Begin : Version, End : Version, out T> { | |
data class Exist<Begin : Version, End : Version, T>(val data: T) : ValidBetween<Begin, End, T>() | |
data class NotExist<Begin : Version, End : Version>(val unit: Unit = Unit) : ValidBetween<Begin, End, Nothing>() | |
companion object { | |
inline fun <reified Begin : Version, reified End : Version, T> construct(version: Int, data: T?): ValidBetween<Begin, End, T> { | |
val beginVersion = Begin::class.java.getDeclaredField("value").get(null) as Int | |
val endVersion = End::class.java.getDeclaredField("value").get(null) as Int | |
return if (version in beginVersion..endVersion) { | |
Exist<Begin, End, T>(requireNotNull(data)) | |
} else { | |
NotExist() | |
} | |
} | |
} | |
} | |
@Serializable(with = ArticleCreatedEventSerializer::class) | |
data class ArticleCreatedEvent( | |
val eventVersion: Int, | |
val userId: Long, | |
val userVersion: AddedFrom<Version2, Int>, | |
val tag: DeletedFrom<Version3, String>, | |
val comment: ValidBetween<Version2, Version3, String> | |
) | |
@OptIn(ExperimentalSerializationApi::class) | |
@Serializer(forClass = ArticleCreatedEvent::class) | |
object ArticleCreatedEventSerializer : KSerializer<ArticleCreatedEvent> { | |
override fun deserialize(decoder: Decoder): ArticleCreatedEvent { | |
require(decoder is JsonDecoder) | |
val element = decoder.decodeJsonElement() | |
require(element is JsonObject) | |
val eventVersion = requireNotNull(element["eventVersion"]).jsonPrimitive.int | |
return ArticleCreatedEvent( | |
eventVersion, | |
userId = requireNotNull(element["userId"]).jsonPrimitive.long, | |
userVersion = AddedFrom.construct( | |
eventVersion, | |
element["userVersion"]?.jsonPrimitive?.int | |
), | |
tag = DeletedFrom.construct( | |
eventVersion, | |
element["tag"]?.jsonPrimitive?.content | |
), | |
comment = ValidBetween.construct( | |
eventVersion, | |
element["comment"]?.jsonPrimitive?.content | |
), | |
) | |
} | |
override fun serialize(encoder: Encoder, value: ArticleCreatedEvent) { | |
require(encoder is JsonEncoder) | |
encoder.encodeJsonElement(buildJsonObject { | |
put("eventVersion", encoder.json.encodeToJsonElement(value.eventVersion)) | |
put("userId", encoder.json.encodeToJsonElement(value.userId)) | |
when (value.userVersion) { | |
is AddedFrom.Exist -> put("userVersion", encoder.json.encodeToJsonElement(value.userVersion.data)) | |
is AddedFrom.NotExist -> Unit | |
} | |
when (value.tag) { | |
is DeletedFrom.Exist -> put("tag", encoder.json.encodeToJsonElement(value.tag.data)) | |
is DeletedFrom.NotExist -> Unit | |
} | |
when (value.comment) { | |
is ValidBetween.Exist -> put("comment", encoder.json.encodeToJsonElement(value.comment.data)) | |
is ValidBetween.NotExist -> Unit | |
} | |
}) | |
} | |
} | |
class JsonSerializeSampleTest { | |
@Test | |
fun TestSerializeV1() { | |
val jsonString = """ | |
{ | |
"eventVersion": 1, | |
"userId": 100, | |
"tag": "test" | |
} | |
""".trimIndent() | |
val result = Json.decodeFromString<ArticleCreatedEvent>(jsonString) | |
assertThat(result).isEqualTo( | |
ArticleCreatedEvent( | |
eventVersion = 1, | |
userId = 100, | |
userVersion = AddedFrom.NotExist(), | |
tag = DeletedFrom.Exist("test"), | |
comment = ValidBetween.NotExist(), | |
) | |
) | |
} | |
@Test | |
fun TestSerializeV2() { | |
val jsonString = """ | |
{ | |
"eventVersion": 2, | |
"userId": 100, | |
"userVersion": 1, | |
"tag": "test", | |
"comment": "comment" | |
} | |
""".trimIndent() | |
val result = Json.decodeFromString<ArticleCreatedEvent>(jsonString) | |
assertThat(result).isEqualTo( | |
ArticleCreatedEvent( | |
eventVersion = 2, | |
userId = 100, | |
userVersion = AddedFrom.Exist(1), | |
tag = DeletedFrom.Exist("test"), | |
comment = ValidBetween.Exist("comment"), | |
) | |
) | |
} | |
@Test | |
fun TestSerializeV3() { | |
val jsonString = """ | |
{ | |
"eventVersion": 3, | |
"userId": 100, | |
"userVersion": 1, | |
"tag": "test", | |
"comment": "comment" | |
} | |
""".trimIndent() | |
val result = Json.decodeFromString<ArticleCreatedEvent>(jsonString) | |
assertThat(result).isEqualTo( | |
ArticleCreatedEvent( | |
eventVersion = 3, | |
userId = 100, | |
userVersion = AddedFrom.Exist(1), | |
tag = DeletedFrom.NotExist(), | |
comment = ValidBetween.Exist("comment"), | |
) | |
) | |
} | |
@Test | |
fun TestSerializeV4() { | |
val jsonString = """ | |
{ | |
"eventVersion": 4, | |
"userId": 100, | |
"userVersion": 1, | |
"tag": "test" | |
} | |
""".trimIndent() | |
val result = Json.decodeFromString<ArticleCreatedEvent>(jsonString) | |
assertThat(result).isEqualTo( | |
ArticleCreatedEvent( | |
eventVersion = 4, | |
userId = 100, | |
userVersion = AddedFrom.Exist(1), | |
tag = DeletedFrom.NotExist(), | |
comment = ValidBetween.NotExist(), | |
) | |
) | |
} | |
@Test | |
fun TestDeserializeV1() { | |
val event = ArticleCreatedEvent( | |
eventVersion = 1, | |
userId = 100, | |
userVersion = AddedFrom.NotExist(), | |
tag = DeletedFrom.Exist("tag"), | |
comment = ValidBetween.NotExist(), | |
) | |
val result = Json.encodeToString(event) | |
assertThat(result).isEqualTo( | |
""" | |
{ | |
"eventVersion": 1, | |
"userId": 100, | |
"tag": "tag" | |
} | |
""".replace(Regex("\\s"), "") | |
) | |
} | |
@Test | |
fun TestDeserializeV2() { | |
val event = ArticleCreatedEvent( | |
eventVersion = 2, | |
userId = 100, | |
userVersion = AddedFrom.Exist(1), | |
tag = DeletedFrom.Exist("tag"), | |
comment = ValidBetween.Exist("comment"), | |
) | |
val result = Json.encodeToString(event) | |
assertThat(result).isEqualTo( | |
""" | |
{ | |
"eventVersion": 2, | |
"userId": 100, | |
"userVersion": 1, | |
"tag": "tag", | |
"comment": "comment" | |
} | |
""".replace(Regex("\\s"), "") | |
) | |
} | |
@Test | |
fun TestDeserializeV3() { | |
val event = ArticleCreatedEvent( | |
eventVersion = 3, | |
userId = 100, | |
userVersion = AddedFrom.Exist(1), | |
tag = DeletedFrom.NotExist(), | |
comment = ValidBetween.Exist("comment"), | |
) | |
val result = Json.encodeToString(event) | |
assertThat(result).isEqualTo( | |
""" | |
{ | |
"eventVersion": 3, | |
"userId": 100, | |
"userVersion": 1, | |
"comment": "comment" | |
} | |
""".replace(Regex("\\s"), "") | |
) | |
} | |
@Test | |
fun TestDeserializeV4() { | |
val event = ArticleCreatedEvent( | |
eventVersion = 4, | |
userId = 100, | |
userVersion = AddedFrom.Exist(1), | |
tag = DeletedFrom.NotExist(), | |
comment = ValidBetween.NotExist(), | |
) | |
val result = Json.encodeToString(event) | |
assertThat(result).isEqualTo( | |
""" | |
{ | |
"eventVersion": 4, | |
"userId": 100, | |
"userVersion": 1 | |
} | |
""".replace(Regex("\\s"), "") | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment