Skip to content

Commit

Permalink
Merge pull request #419 from efenderbosch/inline-unsigned-numbers
Browse files Browse the repository at this point in the history
added (de)serializers for Kotlin unsigned number types
  • Loading branch information
dinomite authored Feb 28, 2021
2 parents a1d685f + 02698b8 commit 5c87444
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 19 deletions.
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>

This comment has been minimized.

Copy link
@cowtowncoder

cowtowncoder Feb 28, 2021

Member

Whops. Assuming accidental, will change back to 2.12.2-SNAPSHOT

This comment has been minimized.

Copy link
@dinomite

dinomite Mar 1, 2021

Author Member

Ooh yep, my fault, I changed the base branch of the PR but didn't fix this.

This comment has been minimized.

Copy link
@cowtowncoder

cowtowncoder Mar 1, 2021

Member

yeah figured that must have happened. Just noticed it based on a deprecation warning on compile which was lucky :)

</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
5 changes: 5 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ Authors:

Contributors:

Eric Fenderbosch (efenderbosch@github)
* Fixed #182: Serialize unsigned numbers
(2.12.next)


Elisha Peterson (triathematician@github)
* Reported #409: `module-info.java` missing "exports"
(2.12.2)
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
}
}
}
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)
}
}
}

0 comments on commit 5c87444

Please sign in to comment.