Skip to content

Commit

Permalink
Add jackson module for serialization and deserialization (#3)
Browse files Browse the repository at this point in the history
Add jackson module for serialization and deserialization
  • Loading branch information
miensol committed Jun 27, 2023
1 parent acf8545 commit 2eb18ef
Show file tree
Hide file tree
Showing 13 changed files with 374 additions and 8 deletions.
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ object Dependencies {
const val shouldko = "com.github.miensol:shouldko:${Versions.shouldko}"
const val serializationCore = "org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.serialization}"
const val serializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serialization}"
const val jackson = "com.fasterxml.jackson.core:jackson-databind:${Versions.jackson}"
}
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ object Versions {
const val junit = "5.6.2"
const val shouldko = "0.2.2"
const val serialization = "1.3.2"
}
const val jackson = "2.15.2"
}
1 change: 1 addition & 0 deletions enums-jackson/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
16 changes: 16 additions & 0 deletions enums-jackson/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
kotlin("jvm")
id("default-config")
id("default-java-publish")
}

dependencies {
api(rootProject)
api(project(":enums"))

implementation(kotlin("stdlib"))
implementation(Dependencies.jackson)

testImplementation(Dependencies.junit)
testImplementation(Dependencies.shouldko)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package pl.brightinventions.codified.jackson

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import pl.brightinventions.codified.Codified
import pl.brightinventions.codified.enums.CodifiedEnumDecoder
import java.lang.reflect.ParameterizedType

class CodifiedDeserializer<TEnum, TCode : Any>() : JsonDeserializer<TEnum>(),
ContextualDeserializer where TEnum : Codified<TCode>, TEnum : Enum<TEnum> {
private lateinit var enumCodeType: Class<TCode>
private lateinit var enumType: Class<TEnum>

override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): TEnum? {
@Suppress("UNCHECKED_CAST")
return CodifiedEnumDecoder.decode(p.readValueAs(enumCodeType), enumType).knownOrNull()
}

override fun createContextual(
ctxt: DeserializationContext,
property: BeanProperty?
): JsonDeserializer<TEnum> {
return CodifiedDeserializer(ctxt.contextualType)
}

override fun handledType(): Class<*> {
return Codified::class.java
}

@Suppress("UNCHECKED_CAST")
constructor(contextualType: JavaType) : this() {
this.enumType = contextualType.rawClass as Class<TEnum>
this.enumCodeType = (enumType.genericInterfaces[0] as ParameterizedType).actualTypeArguments[0] as Class<TCode>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package pl.brightinventions.codified.jackson

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import pl.brightinventions.codified.Codified
import pl.brightinventions.codified.enums.CodifiedEnum
import pl.brightinventions.codified.enums.CodifiedEnumDecoder
import java.io.Serializable

internal class CodifiedEnumDeserializer<TEnum, TCode : Any>() : JsonDeserializer<CodifiedEnum<TEnum, TCode>>(),
ContextualDeserializer where TEnum : Codified<TCode>, TEnum : Enum<TEnum> {
private lateinit var enumCodeType: Class<TCode>
private lateinit var enumType: Class<TEnum>

override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): CodifiedEnum<TEnum, TCode> {
@Suppress("UNCHECKED_CAST")
return CodifiedEnumDecoder.decode(p.readValueAs(enumCodeType), enumType)
}

override fun createContextual(
ctxt: DeserializationContext,
property: BeanProperty?
): JsonDeserializer<CodifiedEnum<TEnum, TCode>> {
return CodifiedEnumDeserializer(ctxt.contextualType)
}

@Suppress("UNCHECKED_CAST")
constructor(contextualType: JavaType) : this() {
this.enumType = contextualType.bindings.getBoundType(0).rawClass as Class<TEnum>
this.enumCodeType = contextualType.bindings.getBoundType(1).rawClass as Class<TCode>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pl.brightinventions.codified.jackson

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import pl.brightinventions.codified.enums.CodifiedEnum

internal class CodifiedEnumSerializer : StdSerializer<CodifiedEnum<*, *>>(CodifiedEnum::class.java) {
override fun serialize(value: CodifiedEnum<*, *>, gen: JsonGenerator, serializers: SerializerProvider) {
serializers.defaultSerializeValue(value.code(), gen)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package pl.brightinventions.codified.jackson

import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.Module
import com.fasterxml.jackson.databind.module.SimpleDeserializers
import com.fasterxml.jackson.databind.module.SimpleSerializers
import pl.brightinventions.codified.Codified
import pl.brightinventions.codified.enums.CodifiedEnum

class CodifiedJacksonModule : Module() {
override fun version(): Version {
return Version.unknownVersion()
}

override fun getModuleName(): String {
return CodifiedJacksonModule::class.qualifiedName!!
}

override fun setupModule(context: SetupContext) {
context.addSerializers(
SimpleSerializers(
listOf(
CodifiedEnumSerializer(),
CodifiedSerializer()
)
)
)

val simpleDeserializers = SimpleDeserializers().apply {
addDeserializer(CodifiedEnum::class.java, CodifiedEnumDeserializer::class.java.newInstance())
addDeserializer(Codified::class.java, CodifiedDeserializer::class.java.newInstance())
}
context.addDeserializers(simpleDeserializers)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package pl.brightinventions.codified.jackson

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import pl.brightinventions.codified.Codified

internal class CodifiedSerializer : StdSerializer<Codified<*>>(Codified::class.java) {
override fun serialize(value: Codified<*>, gen: JsonGenerator, serializers: SerializerProvider) {
serializers.defaultSerializeValue(value.code, gen)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pl.brightinventions.codified.jackson.CodifiedJacksonModule
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package pl.brightinventions.codified.jackson

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import org.junit.jupiter.api.Test
import pl.brightinventions.codified.Codified
import pl.brightinventions.codified.enums.CodifiedEnum
import pl.miensol.shouldko.shouldEqual

class CodifiedJacksonModuleTest {
val objectMapper = ObjectMapper().findAndRegisterModules()

@Test
fun `can serialize and deserialize known value to codified enum with string`() {
// given
val input = Colour.Blue

// when
val output = serdeCodifiedEnum(input)

// then
output.knownOrNull().shouldEqual(input)
}

@Test
fun `can serialize and deserialize known value to codified enum with int`() {
// given
val input = Weight.Heavy

// when
val output = serdeCodifiedEnum(input)

// then
output.knownOrNull().shouldEqual(input)
}

@Test
fun `can serialize and deserialize not known value to codified enum`() {
// given
val input = "pink"

// when
val output = serdeCodifiedEnum(input)

// then
output.knownOrNull().shouldEqual(null)
output.code().shouldEqual("pink")
}

@Test
fun `can serialize and deserialize known value to enum property with string`() {
// given
val input = HasColour(Colour.Blue)

// when
val output = serde(input)

// then
output.shouldEqual(input)
}

@Test
fun `can serialize and deserialize known value to enum property with int`() {
// given
val input = HasWeight().apply { weight = Weight.Medium }

// when
val output = serde(input)

// then
output.shouldEqual(input)
}

@Test
fun `can deserialize not known value to enum property`() {
// given
val input = """{"colour": "pink"}"""
// when
val output = objectMapper.readValue(input, HasColour::class.java)
// then
output.colour.shouldEqual(null)
}

@Test
fun `can deserialize known value to enum property`() {
// given
val input = """{"colour": "blue"}"""
// when
val output = objectMapper.readValue(input, HasColour::class.java)
// then
output.colour.shouldEqual(Colour.Blue)
}

@Test
fun `can deserialize known value to codified enum property`() {
// given
val input = HasCodifiedColour(Colour.Blue)

// when
val output = serde(input)

// then
output.shouldEqual(input)
}

@Test
fun `can deserialize not known value to codified enum property`() {
// given
val input = HasCodifiedColour("pink")
// when
val output = serde(input)
// then
output.colour!!.knownOrNull().shouldEqual(null)
output.colour!!.code().shouldEqual("pink")
}

private fun serdeCodifiedEnum(input: Colour): CodifiedEnum<Colour, String> {
val json = objectMapper.writer().writeValueAsString(input)
return objectMapper.readValue(json, object : TypeReference<CodifiedEnum<Colour, String>>() {})
}

private fun serdeCodifiedEnum(input: Weight): CodifiedEnum<Weight, Int> {
val json = objectMapper.writer().writeValueAsString(input)
return objectMapper.readValue(json, object : TypeReference<CodifiedEnum<Weight, Int>>() {})
}

private fun serdeCodifiedEnum(input: String): CodifiedEnum<Colour, String> {
val json = objectMapper.writer().writeValueAsString(input)
return objectMapper.readValue(json, object : TypeReference<CodifiedEnum<Colour, String>>() {})
}

private inline fun <reified T> serde(input: T): T {
val json = objectMapper.writer().writeValueAsString(input)
return objectMapper.readValue(json, T::class.java)
}
}


@JsonDeserialize(using = CodifiedDeserializer::class)
enum class Colour(override val code: String) : Codified<String> {
Red("red"), Green("green"), Blue("blue");
}

class HasColour() {
var colour: Colour? = null

constructor(colour: Colour) : this() {
this.colour = colour
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as HasColour

return colour == other.colour
}

override fun hashCode(): Int {
return colour?.hashCode() ?: 0
}
}

class HasCodifiedColour() {
var colour: CodifiedEnum<Colour, String>? = null

constructor(colour: Colour) : this() {
this.colour = CodifiedEnum.Known(colour)
}

constructor(colour: String) : this() {
this.colour = CodifiedEnum.Unknown(colour)
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as HasCodifiedColour

return colour == other.colour
}

override fun hashCode(): Int {
return colour?.hashCode() ?: 0
}
}


@JsonDeserialize(using = CodifiedDeserializer::class)
enum class Weight(override val code: Int) : Codified<Int> {
Light(200), Medium(400), Heavy(60);
}

class HasWeight {
var weight: Weight? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as HasWeight

return weight == other.weight
}

override fun hashCode(): Int {
return weight?.hashCode() ?: 0
}
}

Loading

0 comments on commit 2eb18ef

Please sign in to comment.