Skip to content

Commit a94ff4d

Browse files
committed
handle nullable kotlin types and json nulls correctly
1 parent 65c8024 commit a94ff4d

File tree

2 files changed

+50
-29
lines changed

2 files changed

+50
-29
lines changed

src/main/kotlin/com/github/jsonj/JsonJExtensions.kt

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import kotlin.reflect.full.isSubtypeOf
1616
import kotlin.reflect.full.memberProperties
1717
import kotlin.reflect.full.primaryConstructor
1818
import kotlin.reflect.full.starProjectedType
19+
import kotlin.reflect.full.withNullability
1920
import kotlin.reflect.jvm.jvmErasure
2021

2122
fun obj(init: JsonObject.() -> Unit): JsonObject {
@@ -51,10 +52,16 @@ fun JsonObject.arrayField(key: String, vararg values: Any) {
5152
fun JsonObject.flexGet(name: String, ignoreCase: Boolean = true, ignoreUnderscores: Boolean = true): JsonElement? {
5253
val key = keys.filter { normalize(it, ignoreCase, ignoreUnderscores) == normalize(name, ignoreCase, ignoreUnderscores) }
5354
.firstOrNull()
54-
if (key != null) {
55-
return get(key)
55+
return if (key != null) {
56+
val value = get(key)
57+
if (value?.isNull() == true) {
58+
// handle json null as null
59+
null
60+
} else {
61+
value
62+
}
5663
} else {
57-
return null
64+
null
5865
}
5966
}
6067

@@ -73,25 +80,26 @@ fun <T : Any> JsonObject.construct(clazz: KClass<T>): T {
7380

7481
primaryConstructor!!.parameters.forEach {
7582
val name = it.name.orEmpty()
76-
if (it.type.isSubtypeOf(Int::class.starProjectedType)) {
83+
val nonNullableType = it.type.withNullability(false)
84+
if (nonNullableType.isSubtypeOf(Int::class.starProjectedType)) {
7785
paramz.put(it, flexGet(name)?.asInt())
78-
} else if (it.type.isSubtypeOf(Long::class.starProjectedType)) {
86+
} else if (nonNullableType.isSubtypeOf(Long::class.starProjectedType)) {
7987
paramz.put(it, flexGet(name)?.asLong())
80-
} else if (it.type.isSubtypeOf(Float::class.starProjectedType)) {
88+
} else if (nonNullableType.isSubtypeOf(Float::class.starProjectedType)) {
8189
paramz.put(it, flexGet(name)?.asFloat())
82-
} else if (it.type.isSubtypeOf(Double::class.starProjectedType)) {
90+
} else if (nonNullableType.isSubtypeOf(Double::class.starProjectedType)) {
8391
paramz.put(it, flexGet(name)?.asDouble())
84-
} else if (it.type.isSubtypeOf(BigInteger::class.starProjectedType)) {
92+
} else if (nonNullableType.isSubtypeOf(BigInteger::class.starProjectedType)) {
8593
paramz.put(it, flexGet(name)?.asNumber())
86-
} else if (it.type.isSubtypeOf(BigDecimal::class.starProjectedType)) {
94+
} else if (nonNullableType.isSubtypeOf(BigDecimal::class.starProjectedType)) {
8795
paramz.put(it, flexGet(name)?.asNumber())
88-
} else if (it.type.isSubtypeOf(Long::class.starProjectedType)) {
96+
} else if (nonNullableType.isSubtypeOf(Long::class.starProjectedType)) {
8997
paramz.put(it, flexGet(name)?.asLong())
90-
} else if (it.type.isSubtypeOf(String::class.starProjectedType)) {
98+
} else if (nonNullableType.isSubtypeOf(String::class.starProjectedType)) {
9199
paramz.put(it, flexGet(name)?.asString())
92-
} else if (it.type.isSubtypeOf(Boolean::class.starProjectedType)) {
100+
} else if (nonNullableType.isSubtypeOf(Boolean::class.starProjectedType)) {
93101
paramz.put(it, flexGet(name)?.asBoolean())
94-
} else if (it.type.isSubtypeOf(Enum::class.starProjectedType)) {
102+
} else if (nonNullableType.isSubtypeOf(Enum::class.starProjectedType)) {
95103
val enumName = flexGet(name)?.asString()
96104
if (enumName != null) {
97105
@Suppress("UNCHECKED_CAST") // we already checked but too hard for Kotlin to figure out
@@ -144,21 +152,24 @@ private fun toUnderscore(propertyName: String): String {
144152

145153
private fun jsonElement(returnType: KType, value: Any?): JsonElement {
146154
val jsonElement: JsonElement
147-
if (value == null) return nullValue()
148-
if (returnType.isSubtypeOf(Number::class.starProjectedType) ||
149-
returnType.isSubtypeOf(String::class.starProjectedType) ||
150-
returnType.isSubtypeOf(Boolean::class.starProjectedType)
155+
if (value == null) {
156+
return nullValue()
157+
}
158+
val nonNullableReturnType = returnType.withNullability(false)
159+
if (nonNullableReturnType.isSubtypeOf(Number::class.starProjectedType) ||
160+
nonNullableReturnType.isSubtypeOf(String::class.starProjectedType) ||
161+
nonNullableReturnType.isSubtypeOf(Boolean::class.starProjectedType)
151162
) {
152163
jsonElement = primitive(value)
153-
} else if (returnType.isSubtypeOf(Collection::class.starProjectedType)) {
164+
} else if (nonNullableReturnType.isSubtypeOf(Collection::class.starProjectedType)) {
154165
val arr = array()
155166
Collection::class.cast(value).forEach {
156167
if (it != null) {
157168
arr.add(jsonElement(it::class.starProjectedType, it))
158169
}
159170
}
160171
jsonElement = arr
161-
} else if (returnType.isSubtypeOf(Map::class.starProjectedType)) {
172+
} else if (nonNullableReturnType.isSubtypeOf(Map::class.starProjectedType)) {
162173
val newObj = JsonObject()
163174
Map::class.cast(value).forEach {
164175
if (it.key != null) {

src/test/kotlin/com/github/jsonj/JsonJAdaptableTest.kt

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ enum class MyEnum { FOO, BAR }
99

1010
data class Foo(
1111
val message: String,
12+
val message2: String?,
13+
val message3: String?,
1214
val value: Int,
1315
val value2: Long,
1416
val value3: Float,
@@ -23,17 +25,25 @@ class JsonJAdaptableTest {
2325
@Test
2426
fun shouldConvert() {
2527
val foo = Foo(
26-
"meaning",
27-
42,
28-
42,
29-
42.0f,
30-
42.0,
31-
BigInteger.valueOf(2).pow(128),
32-
BigDecimal.valueOf(42.666).pow(100),
33-
true,
34-
MyEnum.FOO
28+
message = "meaning",
29+
message2 = "foo",
30+
message3 = null,
31+
value = 42,
32+
value2 = 42,
33+
value3 = 42.0f,
34+
value4 = 42.0,
35+
value5 = BigInteger.valueOf(2).pow(128),
36+
value6 = BigDecimal.valueOf(42.666).pow(100),
37+
maybe = true,
38+
numnun = MyEnum.FOO
3539
)
3640
val json = foo.asJsonObject()
37-
assertThat(json.construct(Foo::class)).isEqualTo(foo)
41+
val reconstructed = json.construct(Foo::class)
42+
assertThat(foo.message2).isNotBlank()
43+
assertThat(foo.message3).isNull()
44+
assertThat(json.get("message3")).isNull()
45+
assertThat(reconstructed.message2).isEqualTo("foo")
46+
assertThat(reconstructed.message3).isNull()
47+
assertThat(reconstructed).isEqualTo(foo)
3848
}
3949
}

0 commit comments

Comments
 (0)