Skip to content

Commit

Permalink
added (de)serializers for Kotlin unsigned number types
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Fenderbosch committed Feb 26, 2021
1 parent c2be1f4 commit 787c887
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")

package com.fasterxml.jackson.module.kotlin

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT
import com.fasterxml.jackson.core.exc.InputCoercionException
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.Deserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
Expand All @@ -13,7 +15,7 @@ object SequenceDeserializer : StdDeserializer<Sequence<*>>(Sequence::class.java)
}
}

object RegexDeserializer: StdDeserializer<Regex>(Regex::class.java) {
object RegexDeserializer : StdDeserializer<Regex>(Regex::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Regex {
val node = ctxt.readTree(p)

Expand All @@ -37,14 +39,61 @@ object RegexDeserializer: StdDeserializer<Regex>(Regex::class.java) {
}
}

internal class KotlinDeserializers: Deserializers.Base() {
override fun findBeanDeserializer(type: JavaType, config: DeserializationConfig?, beanDesc: BeanDescription?): JsonDeserializer<*>? {
return if (type.isInterface && type.rawClass == Sequence::class.java) {
SequenceDeserializer
} else if (type.rawClass == Regex::class.java) {
RegexDeserializer
} else {
null
object UByteDeserializer : StdDeserializer<UByte>(UByte::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
p.shortValue.asUByte() ?: throw InputCoercionException(
p,
"Numeric value (${p.text}) out of range of UByte (0 - ${UByte.MAX_VALUE}).",
VALUE_NUMBER_INT,
UByte::class.java
)
}

object UShortDeserializer : StdDeserializer<UShort>(UShort::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
p.intValue.asUShort() ?: throw InputCoercionException(
p,
"Numeric value (${p.text}) out of range of UShort (0 - ${UShort.MAX_VALUE}).",
VALUE_NUMBER_INT,
UShort::class.java
)
}

object UIntDeserializer : StdDeserializer<UInt>(UInt::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
p.longValue.asUInt() ?: throw InputCoercionException(
p,
"Numeric value (${p.text}) out of range of UInt (0 - ${UInt.MAX_VALUE}).",
VALUE_NUMBER_INT,
UInt::class.java
)
}

object ULongDeserializer : StdDeserializer<ULong>(ULong::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
p.bigIntegerValue.asULong() ?: throw InputCoercionException(
p,
"Numeric value (${p.text}) out of range of ULong (0 - ${ULong.MAX_VALUE}).",
VALUE_NUMBER_INT,
ULong::class.java
)
}

@ExperimentalUnsignedTypes
internal class KotlinDeserializers : Deserializers.Base() {
override fun findBeanDeserializer(
type: JavaType,
config: DeserializationConfig?,
beanDesc: BeanDescription?
): JsonDeserializer<*>? {
return when {
type.isInterface && type.rawClass == Sequence::class.java -> SequenceDeserializer
type.rawClass == Regex::class.java -> RegexDeserializer
type.rawClass == UByte::class.java -> UByteDeserializer
type.rawClass == UShort::class.java -> UShortDeserializer
type.rawClass == UInt::class.java -> UIntDeserializer
type.rawClass == ULong::class.java -> ULongDeserializer
else -> null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")

package com.fasterxml.jackson.module.kotlin

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.ser.Serializers
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.math.BigInteger

object SequenceSerializer : StdSerializer<Sequence<*>>(Sequence::class.java) {
override fun serialize(value: Sequence<*>, gen: JsonGenerator, provider: SerializerProvider) {
Expand All @@ -12,12 +15,43 @@ object SequenceSerializer : StdSerializer<Sequence<*>>(Sequence::class.java) {
}
}

internal class KotlinSerializers : Serializers.Base() {
override fun findSerializer(config: SerializationConfig?, type: JavaType, beanDesc: BeanDescription?): JsonSerializer<*>? {
return if (Sequence::class.java.isAssignableFrom(type.rawClass)) {
SequenceSerializer
} else {
null
object UByteSerializer : StdSerializer<UByte>(UByte::class.java) {
override fun serialize(value: UByte, gen: JsonGenerator, provider: SerializerProvider) =
gen.writeNumber(value.toShort())
}

object UShortSerializer : StdSerializer<UShort>(UShort::class.java) {
override fun serialize(value: UShort, gen: JsonGenerator, provider: SerializerProvider) =
gen.writeNumber(value.toInt())
}

object UIntSerializer : StdSerializer<UInt>(UInt::class.java) {
override fun serialize(value: UInt, gen: JsonGenerator, provider: SerializerProvider) =
gen.writeNumber(value.toLong())
}

object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
override fun serialize(value: ULong, gen: JsonGenerator, provider: SerializerProvider) {
val longValue = value.toLong()
when {
longValue >= 0 -> gen.writeNumber(longValue)
else -> gen.writeNumber(BigInteger(value.toString()))
}
}
}

@Suppress("EXPERIMENTAL_API_USAGE")
internal class KotlinSerializers : Serializers.Base() {
override fun findSerializer(
config: SerializationConfig?,
type: JavaType,
beanDesc: BeanDescription?
): JsonSerializer<*>? = when {
Sequence::class.java.isAssignableFrom(type.rawClass) -> SequenceSerializer
UByte::class.java.isAssignableFrom(type.rawClass) -> UByteSerializer
UShort::class.java.isAssignableFrom(type.rawClass) -> UShortSerializer
UInt::class.java.isAssignableFrom(type.rawClass) -> UIntSerializer
ULong::class.java.isAssignableFrom(type.rawClass) -> ULongSerializer
else -> null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")

package com.fasterxml.jackson.module.kotlin

import java.math.BigInteger

fun Short.asUByte() = when {
this >= 0 && this <= UByte.MAX_VALUE.toShort() -> this.toUByte()
else -> null
}

fun Int.asUShort() = when {
this >= 0 && this <= UShort.MAX_VALUE.toInt() -> this.toUShort()
else -> null
}

fun Long.asUInt() = when {
this >= 0 && this <= UInt.MAX_VALUE.toLong() -> this.toUInt()
else -> null
}

private val uLongMaxValue = BigInteger(ULong.MAX_VALUE.toString())
fun BigInteger.asULong() = when {
this >= BigInteger.ZERO && this <= uLongMaxValue -> this.toLong().toULong()
else -> null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")

package com.fasterxml.jackson.module.kotlin.test

import com.fasterxml.jackson.core.exc.InputCoercionException
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.junit.Test
import java.math.BigInteger
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertThrows

internal class UnsignedNumbersTests {

val mapper: ObjectMapper = jacksonObjectMapper()

@Test
fun `test UByte`() {
val json = mapper.writeValueAsString(UByte.MAX_VALUE)
val deserialized = mapper.readValue<UByte>(json)
assertThat(deserialized, equalTo(UByte.MAX_VALUE))
}

@Test
fun `test UByte overflow`() {
val json = mapper.writeValueAsString(UByte.MAX_VALUE + 1u)
assertThrows(InputCoercionException::class.java) { mapper.readValue<UByte>(json) }
}

@Test
fun `test UByte underflow`() {
val json = mapper.writeValueAsString(-1)
assertThrows(InputCoercionException::class.java) { mapper.readValue<UByte>(json) }
}

@Test
fun `test UShort`() {
val json = mapper.writeValueAsString(UShort.MAX_VALUE)
val deserialized = mapper.readValue<UShort>(json)
assertThat(deserialized, equalTo(UShort.MAX_VALUE))
}

@Test
fun `test UShort overflow`() {
val json = mapper.writeValueAsString(UShort.MAX_VALUE + 1u)
assertThrows(InputCoercionException::class.java) { mapper.readValue<UShort>(json) }
}

@Test
fun `test UShort underflow`() {
val json = mapper.writeValueAsString(-1)
assertThrows(InputCoercionException::class.java) { mapper.readValue<UShort>(json) }
}

@Test
fun `test UInt`() {
val json = mapper.writeValueAsString(UInt.MAX_VALUE)
val deserialized = mapper.readValue<UInt>(json)
assertThat(deserialized, equalTo(UInt.MAX_VALUE))
}

@Test
fun `test UInt overflow`() {
val json = mapper.writeValueAsString(UInt.MAX_VALUE.toULong() + 1u)
assertThrows(InputCoercionException::class.java) { mapper.readValue<UInt>(json) }
}

@Test
fun `test UInt underflow`() {
val json = mapper.writeValueAsString(-1)
assertThrows(InputCoercionException::class.java) { mapper.readValue<UInt>(json) }
}

@Test
fun `test ULong`() {
val json = mapper.writeValueAsString(ULong.MAX_VALUE)
val deserialized = mapper.readValue<ULong>(json)
assertThat(deserialized, equalTo(ULong.MAX_VALUE))
}

@Test
fun `test ULong overflow`() {
val value = BigInteger(ULong.MAX_VALUE.toString()) + BigInteger.ONE
val json = mapper.writeValueAsString(value)
assertThrows(InputCoercionException::class.java) { mapper.readValue<ULong>(json) }
}

@Test
fun `test ULong underflow`() {
val json = mapper.writeValueAsString(-1)
assertThrows(InputCoercionException::class.java) { mapper.readValue<ULong>(json) }
}
}

0 comments on commit 787c887

Please sign in to comment.