Skip to content

Commit

Permalink
Non-empty collections for Jackson
Browse files Browse the repository at this point in the history
  • Loading branch information
serras committed Feb 8, 2025
1 parent 3195967 commit 53a79aa
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ public fun ObjectMapper.registerArrowModule(
eitherModuleConfig: EitherModuleConfig = EitherModuleConfig("left", "right"),
iorModuleConfig: IorModuleConfig = IorModuleConfig("left", "right"),
): ObjectMapper = registerModules(
// no longer required, as they are value classes
// NonEmptyListModule,
// NonEmptySetModule,
NonEmptyCollectionsModule(),
OptionModule,
EitherModule(eitherModuleConfig.leftFieldName, eitherModuleConfig.rightFieldName),
IorModule(iorModuleConfig.leftFieldName, iorModuleConfig.rightFieldName),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package arrow.integrations.jackson.module

import arrow.core.NonEmptyCollection
import arrow.core.NonEmptyList
import arrow.core.NonEmptySet
import arrow.core.toNonEmptyListOrNull
import arrow.core.toNonEmptySetOrNull
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.json.PackageVersion
import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.DeserializationConfig
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.deser.Deserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer
import com.fasterxml.jackson.databind.jsontype.TypeSerializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.databind.type.CollectionType

public class NonEmptyCollectionsModule : SimpleModule(NonEmptyCollectionsModule::class.java.name, PackageVersion.VERSION) {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.addSerializers(NonEmptyCollectionSerializerResolver)
context.addDeserializers(NonEmptyCollectionDeserializerResolver)
}
}

public object NonEmptyCollectionSerializerResolver : Serializers.Base() {
override fun findCollectionSerializer(
config: SerializationConfig,
type: CollectionType,
beanDesc: BeanDescription?,
elementTypeSerializer: TypeSerializer?,
elementValueSerializer: JsonSerializer<Any>?
): JsonSerializer<*>? = when {
NonEmptyCollection::class.java.isAssignableFrom(type.rawClass) -> NonEmptyCollectionSerializer
else -> null
}
}

public object NonEmptyCollectionDeserializerResolver : Deserializers.Base() {
override fun findCollectionDeserializer(
type: CollectionType,
config: DeserializationConfig,
beanDesc: BeanDescription?,
elementTypeDeserializer: TypeDeserializer?,
elementDeserializer: JsonDeserializer<*>?
): JsonDeserializer<*>? = when {
NonEmptyList::class.java.isAssignableFrom(type.rawClass) ->
NonEmptyCollectionDeserializer(type.contentType, NonEmptyList::class.java) { it.toNonEmptyListOrNull() }
NonEmptySet::class.java.isAssignableFrom(type.rawClass) ->
NonEmptyCollectionDeserializer(type.contentType, NonEmptySet::class.java) { it.toNonEmptySetOrNull() }
else -> null
}
}

public object NonEmptyCollectionSerializer: StdSerializer<NonEmptyCollection<*>>(NonEmptyCollection::class.java) {
override fun serialize(value: NonEmptyCollection<*>, gen: JsonGenerator, provider: SerializerProvider) {
provider.defaultSerializeValue(value.toList(), gen)
}
}

public class NonEmptyCollectionDeserializer<T: NonEmptyCollection<*>>(
private val contentType: JavaType,
klass: Class<T>,
private val converter: (List<*>) -> T?
) : StdDeserializer<T>(klass) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): T? {
val collection = CollectionType.construct(ArrayList::class.java, contentType)
return converter(ctxt.readValue(p, collection))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import kotlin.test.Ignore

@Ignore
class NonEmptyListModuleTest {
private val mapper = ObjectMapper().registerKotlinModule()
private val mapper = ObjectMapper().registerKotlinModule().registerArrowModule()

@Test
fun `serializing NonEmptyList should be the same as serializing the underlying list`() = runTest {
Expand Down Expand Up @@ -54,13 +53,11 @@ class NonEmptyListModuleTest {
}
}

@Test
@Test @Ignore
fun `serializing NonEmptyList in an object should round trip`() = runTest {
data class Wrapper(val nel: Nel<SomeObject>)

checkAll(arbitrary { Wrapper(Arb.nonEmptyList(Arb.someObject()).bind()) }) { wrapper ->
checkAll(arbitrary { WrapperWithList(Arb.nonEmptyList(Arb.someObject()).bind()) }) { wrapper ->
val encoded: String = mapper.writeValueAsString(wrapper)
val decoded: Wrapper = mapper.readValue(encoded, Wrapper::class.java)
val decoded: WrapperWithList = mapper.readValue(encoded, WrapperWithList::class.java)

decoded shouldBe wrapper
}
Expand All @@ -78,3 +75,5 @@ class NonEmptyListModuleTest {
}
}
}

data class WrapperWithList(val nel: Nel<SomeObject>)
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ import kotlinx.coroutines.test.runTest
import kotlin.test.Ignore
import kotlin.test.Test

@Ignore
class NonEmptySetModuleTest {
private val mapper = ObjectMapper().registerKotlinModule()
private val mapper = ObjectMapper().registerKotlinModule().registerArrowModule()

@Test
fun `serializing NonEmptySet should be the same as serializing the underlying set`() = runTest {
Expand Down Expand Up @@ -57,13 +56,11 @@ class NonEmptySetModuleTest {
}
}

@Test
@Test @Ignore
fun `serializing NonEmptySet in an object should round trip`() = runTest {
data class Wrapper(val nel: NonEmptySet<SomeObject>)

checkAll(arbitrary { Wrapper(Arb.nonEmptySet(Arb.someObject()).bind()) }) { wrapper ->
checkAll(arbitrary { WrapperWithSet(Arb.nonEmptySet(Arb.someObject()).bind()) }) { wrapper ->
val encoded: String = mapper.writeValueAsString(wrapper)
val decoded: Wrapper = mapper.readValue(encoded, Wrapper::class.java)
val decoded: WrapperWithSet = mapper.readValue(encoded, WrapperWithSet::class.java)

decoded shouldBe wrapper
}
Expand All @@ -81,3 +78,5 @@ class NonEmptySetModuleTest {
}
}
}

data class WrapperWithSet(val nel: NonEmptySet<SomeObject>)

0 comments on commit 53a79aa

Please sign in to comment.