Skip to content

Commit

Permalink
Add jackson module for serialization and deserialization
Browse files Browse the repository at this point in the history
  • Loading branch information
miensol committed Jun 23, 2023
1 parent acf8545 commit c40bea4
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 5 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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ package pl.brightinventions.codified.enums
import pl.brightinventions.codified.Codified
import java.util.concurrent.ConcurrentHashMap

@PublishedApi
internal object CodifiedEnumDecoder {
object CodifiedEnumDecoder {
private val enumsSerializedNamed = ConcurrentHashMap<Class<*>, Map<String, Enum<*>>>()

@PublishedApi
internal fun <T> decode(value: String, clazz: Class<T>): CodifiedEnum<T, String>
fun <T> decode(value: String, clazz: Class<T>): CodifiedEnum<T, String>
where T : Enum<T>, T : Codified<String> {

val namesForEnum = enumsSerializedNamed.getOrPut(clazz) {
Expand Down
1 change: 1 addition & 0 deletions jackson/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
16 changes: 16 additions & 0 deletions 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,35 @@
package dev.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

class CodifiedDeserializer<T>() : JsonDeserializer<T>(),
ContextualDeserializer where T : Codified<String>, T : Enum<T> {
private lateinit var enumType: Class<T>

override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): T? {
return CodifiedEnumDecoder.decode(p.valueAsString, enumType).knownOrNull()
}

override fun createContextual(
ctxt: DeserializationContext,
property: BeanProperty?
): JsonDeserializer<T> {
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<T>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.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<T>() : JsonDeserializer<CodifiedEnum<T, String>>(),
ContextualDeserializer where T : Codified<String>, T : Enum<T> {
private lateinit var enumCodeType: Class<Serializable>
private lateinit var enumType: Class<T>

override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): CodifiedEnum<T, String> {
return CodifiedEnumDecoder.decode(p.valueAsString, enumType)
}

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

@Suppress("UNCHECKED_CAST")
constructor(contextualType: JavaType) : this() {
this.enumType = contextualType.bindings.getBoundType(0).rawClass as Class<T>
this.enumCodeType = contextualType.bindings.getBoundType(1).rawClass as Class<Serializable>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.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 dev.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)
}
}

12 changes: 12 additions & 0 deletions jackson/src/main/kotlin/dev/codified/jackson/CodifiedSerializer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.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 @@
dev.codified.jackson.CodifiedJacksonModule
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package dev.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`() {
// given
val input = Colour.Blue

// 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`() {
// given
val input = HasColour(Colour.Blue)

// when
val output = serde(input)

// then
output.shouldEqual(input)
}

@Test
fun `can serialize and 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 serialize and 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 serialize and 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: 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
}


}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
rootProject.name = "codified"
include("enums")
include("enums-serializer")
include("jackson")

0 comments on commit c40bea4

Please sign in to comment.