Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added (de)serializers for Kotlin unsigned number types #419

Merged
merged 8 commits into from
Feb 28, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,28 @@ These Kotlin classes are supported with the following fields for serialization/d

(others are likely to work, but may not be tuned for Jackson)

# Sealed classes without @JsonSubTypes
Subclasses can be detected automatically for sealed classes, since all possible subclasses are known
at compile-time to Kotlin. This makes `com.fasterxml.jackson.annotation.JsonSubTypes` redundant.
A `com.fasterxml.jackson.annotation.@JsonTypeInfo` annotation at the base-class is still necessary.

```kotlin
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
sealed class SuperClass{
class A: SuperClass()
class B: SuperClass()
}

...
val mapper = jacksonObjectMapper()
val root: SuperClass = mapper.readValue(json)
when(root){
is A -> "It's A"
is B -> "It's B"
}
```


# Configuration

The Kotlin module may be given a few configuration parameters at construction time;
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
<parent>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-base</artifactId>
<version>2.12.2-SNAPSHOT</version>
<version>2.13.0-SNAPSHOT</version>
</parent>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<name>jackson-module-kotlin</name>
<version>2.12.2-SNAPSHOT</version>
<version>2.13.0-SNAPSHOT</version>
<packaging>bundle</packaging>
<description>Add-on module for Jackson (https://github.com/FasterXML/jackson/) to support
Kotlin language, specifically introspection of method/constructor parameter names,
Expand Down
4 changes: 4 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Co-maintainers:
=== Releases ===
------------------------------------------------------------------------

2.13.0 (not yet released)

No changes since 2.12

2.12.2 (not yet released)

#409: `module-info.java` missing "exports"
Expand Down
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
}
Comment on lines +89 to 97
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when is one of my favorite Kotlin features, I'm happy every time I see it.

}
}
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,31 @@
package com.fasterxml.jackson.module.kotlin.test

import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.test.SealedClassTest.SuperClass.B
import org.junit.Test
import kotlin.test.assertTrue

class SealedClassTest {
private val mapper = jacksonObjectMapper()

/**
* Json of a Serialized B-Object.
*/
private val jsonB = """{"@type":"SealedClassTest${"$"}SuperClass${"$"}B"}"""

/**
* Tests that the @JsonSubTypes-Annotation is not necessary when working with Sealed-Classes.
*/
@Test
fun sealedClassWithoutSubTypes() {
val result = mapper.readValue(jsonB, SuperClass::class.java)
assertTrue { result is B }
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
sealed class SuperClass {
class A : SuperClass()
class B : SuperClass()
}
}
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) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.fasterxml.jackson.module.kotlin.test.github.failing

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.test.expectFailure
import org.junit.Test
import kotlin.test.assertSame

class TestGithub196 {
@Test
fun testUnitSingletonDeserialization() {
// An empty object should be deserialized as *the* Unit instance, but is not
expectFailure<AssertionError>("GitHub #196 has been fixed!") {
assertSame(jacksonObjectMapper().readValue("{}"), Unit)
}
}
}