Skip to content

Instantly share code, notes, and snippets.

@hnaohiro
Created December 29, 2022 10:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hnaohiro/1d22a5791a7886498d1c52581e157782 to your computer and use it in GitHub Desktop.
Save hnaohiro/1d22a5791a7886498d1c52581e157782 to your computer and use it in GitHub Desktop.
kotlinx.serialization conditional serializer
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