Skip to content

Commit 5c87444

Browse files
authored
Merge pull request #419 from efenderbosch/inline-unsigned-numbers
added (de)serializers for Kotlin unsigned number types
2 parents a1d685f + 02698b8 commit 5c87444

File tree

10 files changed

+303
-19
lines changed

10 files changed

+303
-19
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,28 @@ These Kotlin classes are supported with the following fields for serialization/d
141141

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

144+
# Sealed classes without @JsonSubTypes
145+
Subclasses can be detected automatically for sealed classes, since all possible subclasses are known
146+
at compile-time to Kotlin. This makes `com.fasterxml.jackson.annotation.JsonSubTypes` redundant.
147+
A `com.fasterxml.jackson.annotation.@JsonTypeInfo` annotation at the base-class is still necessary.
148+
149+
```kotlin
150+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
151+
sealed class SuperClass{
152+
class A: SuperClass()
153+
class B: SuperClass()
154+
}
155+
156+
...
157+
val mapper = jacksonObjectMapper()
158+
val root: SuperClass = mapper.readValue(json)
159+
when(root){
160+
is A -> "It's A"
161+
is B -> "It's B"
162+
}
163+
```
164+
165+
144166
# Configuration
145167

146168
The Kotlin module may be given a few configuration parameters at construction time;

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
<parent>
99
<groupId>com.fasterxml.jackson</groupId>
1010
<artifactId>jackson-base</artifactId>
11-
<version>2.12.2-SNAPSHOT</version>
11+
<version>2.13.0-SNAPSHOT</version>
1212
</parent>
1313
<groupId>com.fasterxml.jackson.module</groupId>
1414
<artifactId>jackson-module-kotlin</artifactId>
1515
<name>jackson-module-kotlin</name>
16-
<version>2.12.2-SNAPSHOT</version>
16+
<version>2.13.0-SNAPSHOT</version>
1717
<packaging>bundle</packaging>
1818
<description>Add-on module for Jackson (https://github.com/FasterXML/jackson/) to support
1919
Kotlin language, specifically introspection of method/constructor parameter names,

release-notes/CREDITS-2.x

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Authors:
1313

1414
Contributors:
1515

16+
Eric Fenderbosch (efenderbosch@github)
17+
* Fixed #182: Serialize unsigned numbers
18+
(2.12.next)
19+
20+
1621
Elisha Peterson (triathematician@github)
1722
* Reported #409: `module-info.java` missing "exports"
1823
(2.12.2)

release-notes/VERSION-2.x

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Co-maintainers:
1414
=== Releases ===
1515
------------------------------------------------------------------------
1616

17+
2.13.0 (not yet released)
18+
19+
No changes since 2.12
20+
1721
2.12.2 (not yet released)
1822

1923
#409: `module-info.java` missing "exports"

src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinDeserializers.kt

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
@file:Suppress("EXPERIMENTAL_API_USAGE")
2+
13
package com.fasterxml.jackson.module.kotlin
24

3-
import com.fasterxml.jackson.annotation.JsonCreator
4-
import com.fasterxml.jackson.annotation.JsonProperty
55
import com.fasterxml.jackson.core.JsonParser
6+
import com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT
7+
import com.fasterxml.jackson.core.exc.InputCoercionException
68
import com.fasterxml.jackson.databind.*
79
import com.fasterxml.jackson.databind.deser.Deserializers
810
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
@@ -13,7 +15,7 @@ object SequenceDeserializer : StdDeserializer<Sequence<*>>(Sequence::class.java)
1315
}
1416
}
1517

16-
object RegexDeserializer: StdDeserializer<Regex>(Regex::class.java) {
18+
object RegexDeserializer : StdDeserializer<Regex>(Regex::class.java) {
1719
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Regex {
1820
val node = ctxt.readTree(p)
1921

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

40-
internal class KotlinDeserializers: Deserializers.Base() {
41-
override fun findBeanDeserializer(type: JavaType, config: DeserializationConfig?, beanDesc: BeanDescription?): JsonDeserializer<*>? {
42-
return if (type.isInterface && type.rawClass == Sequence::class.java) {
43-
SequenceDeserializer
44-
} else if (type.rawClass == Regex::class.java) {
45-
RegexDeserializer
46-
} else {
47-
null
42+
object UByteDeserializer : StdDeserializer<UByte>(UByte::class.java) {
43+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
44+
p.shortValue.asUByte() ?: throw InputCoercionException(
45+
p,
46+
"Numeric value (${p.text}) out of range of UByte (0 - ${UByte.MAX_VALUE}).",
47+
VALUE_NUMBER_INT,
48+
UByte::class.java
49+
)
50+
}
51+
52+
object UShortDeserializer : StdDeserializer<UShort>(UShort::class.java) {
53+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
54+
p.intValue.asUShort() ?: throw InputCoercionException(
55+
p,
56+
"Numeric value (${p.text}) out of range of UShort (0 - ${UShort.MAX_VALUE}).",
57+
VALUE_NUMBER_INT,
58+
UShort::class.java
59+
)
60+
}
61+
62+
object UIntDeserializer : StdDeserializer<UInt>(UInt::class.java) {
63+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
64+
p.longValue.asUInt() ?: throw InputCoercionException(
65+
p,
66+
"Numeric value (${p.text}) out of range of UInt (0 - ${UInt.MAX_VALUE}).",
67+
VALUE_NUMBER_INT,
68+
UInt::class.java
69+
)
70+
}
71+
72+
object ULongDeserializer : StdDeserializer<ULong>(ULong::class.java) {
73+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext) =
74+
p.bigIntegerValue.asULong() ?: throw InputCoercionException(
75+
p,
76+
"Numeric value (${p.text}) out of range of ULong (0 - ${ULong.MAX_VALUE}).",
77+
VALUE_NUMBER_INT,
78+
ULong::class.java
79+
)
80+
}
81+
82+
@ExperimentalUnsignedTypes
83+
internal class KotlinDeserializers : Deserializers.Base() {
84+
override fun findBeanDeserializer(
85+
type: JavaType,
86+
config: DeserializationConfig?,
87+
beanDesc: BeanDescription?
88+
): JsonDeserializer<*>? {
89+
return when {
90+
type.isInterface && type.rawClass == Sequence::class.java -> SequenceDeserializer
91+
type.rawClass == Regex::class.java -> RegexDeserializer
92+
type.rawClass == UByte::class.java -> UByteDeserializer
93+
type.rawClass == UShort::class.java -> UShortDeserializer
94+
type.rawClass == UInt::class.java -> UIntDeserializer
95+
type.rawClass == ULong::class.java -> ULongDeserializer
96+
else -> null
4897
}
4998
}
5099
}
Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
@file:Suppress("EXPERIMENTAL_API_USAGE")
2+
13
package com.fasterxml.jackson.module.kotlin
24

35
import com.fasterxml.jackson.core.JsonGenerator
46
import com.fasterxml.jackson.databind.*
57
import com.fasterxml.jackson.databind.ser.Serializers
68
import com.fasterxml.jackson.databind.ser.std.StdSerializer
9+
import java.math.BigInteger
710

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

15-
internal class KotlinSerializers : Serializers.Base() {
16-
override fun findSerializer(config: SerializationConfig?, type: JavaType, beanDesc: BeanDescription?): JsonSerializer<*>? {
17-
return if (Sequence::class.java.isAssignableFrom(type.rawClass)) {
18-
SequenceSerializer
19-
} else {
20-
null
18+
object UByteSerializer : StdSerializer<UByte>(UByte::class.java) {
19+
override fun serialize(value: UByte, gen: JsonGenerator, provider: SerializerProvider) =
20+
gen.writeNumber(value.toShort())
21+
}
22+
23+
object UShortSerializer : StdSerializer<UShort>(UShort::class.java) {
24+
override fun serialize(value: UShort, gen: JsonGenerator, provider: SerializerProvider) =
25+
gen.writeNumber(value.toInt())
26+
}
27+
28+
object UIntSerializer : StdSerializer<UInt>(UInt::class.java) {
29+
override fun serialize(value: UInt, gen: JsonGenerator, provider: SerializerProvider) =
30+
gen.writeNumber(value.toLong())
31+
}
32+
33+
object ULongSerializer : StdSerializer<ULong>(ULong::class.java) {
34+
override fun serialize(value: ULong, gen: JsonGenerator, provider: SerializerProvider) {
35+
val longValue = value.toLong()
36+
when {
37+
longValue >= 0 -> gen.writeNumber(longValue)
38+
else -> gen.writeNumber(BigInteger(value.toString()))
2139
}
2240
}
41+
}
42+
43+
@Suppress("EXPERIMENTAL_API_USAGE")
44+
internal class KotlinSerializers : Serializers.Base() {
45+
override fun findSerializer(
46+
config: SerializationConfig?,
47+
type: JavaType,
48+
beanDesc: BeanDescription?
49+
): JsonSerializer<*>? = when {
50+
Sequence::class.java.isAssignableFrom(type.rawClass) -> SequenceSerializer
51+
UByte::class.java.isAssignableFrom(type.rawClass) -> UByteSerializer
52+
UShort::class.java.isAssignableFrom(type.rawClass) -> UShortSerializer
53+
UInt::class.java.isAssignableFrom(type.rawClass) -> UIntSerializer
54+
ULong::class.java.isAssignableFrom(type.rawClass) -> ULongSerializer
55+
else -> null
56+
}
2357
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@file:Suppress("EXPERIMENTAL_API_USAGE")
2+
3+
package com.fasterxml.jackson.module.kotlin
4+
5+
import java.math.BigInteger
6+
7+
fun Short.asUByte() = when {
8+
this >= 0 && this <= UByte.MAX_VALUE.toShort() -> this.toUByte()
9+
else -> null
10+
}
11+
12+
fun Int.asUShort() = when {
13+
this >= 0 && this <= UShort.MAX_VALUE.toInt() -> this.toUShort()
14+
else -> null
15+
}
16+
17+
fun Long.asUInt() = when {
18+
this >= 0 && this <= UInt.MAX_VALUE.toLong() -> this.toUInt()
19+
else -> null
20+
}
21+
22+
private val uLongMaxValue = BigInteger(ULong.MAX_VALUE.toString())
23+
fun BigInteger.asULong() = when {
24+
this >= BigInteger.ZERO && this <= uLongMaxValue -> this.toLong().toULong()
25+
else -> null
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.fasterxml.jackson.module.kotlin.test
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo
4+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
5+
import com.fasterxml.jackson.module.kotlin.test.SealedClassTest.SuperClass.B
6+
import org.junit.Test
7+
import kotlin.test.assertTrue
8+
9+
class SealedClassTest {
10+
private val mapper = jacksonObjectMapper()
11+
12+
/**
13+
* Json of a Serialized B-Object.
14+
*/
15+
private val jsonB = """{"@type":"SealedClassTest${"$"}SuperClass${"$"}B"}"""
16+
17+
/**
18+
* Tests that the @JsonSubTypes-Annotation is not necessary when working with Sealed-Classes.
19+
*/
20+
@Test
21+
fun sealedClassWithoutSubTypes() {
22+
val result = mapper.readValue(jsonB, SuperClass::class.java)
23+
assertTrue { result is B }
24+
}
25+
26+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
27+
sealed class SuperClass {
28+
class A : SuperClass()
29+
class B : SuperClass()
30+
}
31+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_UNSIGNED_LITERALS")
2+
3+
package com.fasterxml.jackson.module.kotlin.test
4+
5+
import com.fasterxml.jackson.core.exc.InputCoercionException
6+
import com.fasterxml.jackson.databind.ObjectMapper
7+
import com.fasterxml.jackson.databind.SerializationFeature
8+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
9+
import com.fasterxml.jackson.module.kotlin.readValue
10+
import org.junit.Test
11+
import java.math.BigInteger
12+
import org.hamcrest.CoreMatchers.equalTo
13+
import org.hamcrest.MatcherAssert.assertThat
14+
import org.junit.Assert.assertThrows
15+
16+
internal class UnsignedNumbersTests {
17+
18+
val mapper: ObjectMapper = jacksonObjectMapper()
19+
20+
@Test
21+
fun `test UByte`() {
22+
val json = mapper.writeValueAsString(UByte.MAX_VALUE)
23+
val deserialized = mapper.readValue<UByte>(json)
24+
assertThat(deserialized, equalTo(UByte.MAX_VALUE))
25+
}
26+
27+
@Test
28+
fun `test UByte overflow`() {
29+
val json = mapper.writeValueAsString(UByte.MAX_VALUE + 1u)
30+
assertThrows(InputCoercionException::class.java) { mapper.readValue<UByte>(json) }
31+
}
32+
33+
@Test
34+
fun `test UByte underflow`() {
35+
val json = mapper.writeValueAsString(-1)
36+
assertThrows(InputCoercionException::class.java) { mapper.readValue<UByte>(json) }
37+
}
38+
39+
@Test
40+
fun `test UShort`() {
41+
val json = mapper.writeValueAsString(UShort.MAX_VALUE)
42+
val deserialized = mapper.readValue<UShort>(json)
43+
assertThat(deserialized, equalTo(UShort.MAX_VALUE))
44+
}
45+
46+
@Test
47+
fun `test UShort overflow`() {
48+
val json = mapper.writeValueAsString(UShort.MAX_VALUE + 1u)
49+
assertThrows(InputCoercionException::class.java) { mapper.readValue<UShort>(json) }
50+
}
51+
52+
@Test
53+
fun `test UShort underflow`() {
54+
val json = mapper.writeValueAsString(-1)
55+
assertThrows(InputCoercionException::class.java) { mapper.readValue<UShort>(json) }
56+
}
57+
58+
@Test
59+
fun `test UInt`() {
60+
val json = mapper.writeValueAsString(UInt.MAX_VALUE)
61+
val deserialized = mapper.readValue<UInt>(json)
62+
assertThat(deserialized, equalTo(UInt.MAX_VALUE))
63+
}
64+
65+
@Test
66+
fun `test UInt overflow`() {
67+
val json = mapper.writeValueAsString(UInt.MAX_VALUE.toULong() + 1u)
68+
assertThrows(InputCoercionException::class.java) { mapper.readValue<UInt>(json) }
69+
}
70+
71+
@Test
72+
fun `test UInt underflow`() {
73+
val json = mapper.writeValueAsString(-1)
74+
assertThrows(InputCoercionException::class.java) { mapper.readValue<UInt>(json) }
75+
}
76+
77+
@Test
78+
fun `test ULong`() {
79+
val json = mapper.writeValueAsString(ULong.MAX_VALUE)
80+
val deserialized = mapper.readValue<ULong>(json)
81+
assertThat(deserialized, equalTo(ULong.MAX_VALUE))
82+
}
83+
84+
@Test
85+
fun `test ULong overflow`() {
86+
val value = BigInteger(ULong.MAX_VALUE.toString()) + BigInteger.ONE
87+
val json = mapper.writeValueAsString(value)
88+
assertThrows(InputCoercionException::class.java) { mapper.readValue<ULong>(json) }
89+
}
90+
91+
@Test
92+
fun `test ULong underflow`() {
93+
val json = mapper.writeValueAsString(-1)
94+
assertThrows(InputCoercionException::class.java) { mapper.readValue<ULong>(json) }
95+
}
96+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.fasterxml.jackson.module.kotlin.test.github.failing
2+
3+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
4+
import com.fasterxml.jackson.module.kotlin.readValue
5+
import com.fasterxml.jackson.module.kotlin.test.expectFailure
6+
import org.junit.Test
7+
import kotlin.test.assertSame
8+
9+
class TestGithub196 {
10+
@Test
11+
fun testUnitSingletonDeserialization() {
12+
// An empty object should be deserialized as *the* Unit instance, but is not
13+
expectFailure<AssertionError>("GitHub #196 has been fixed!") {
14+
assertSame(jacksonObjectMapper().readValue("{}"), Unit)
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)