Handle nulls symetrically in KotlinJsonAdapter.

When writing nulls we omit them, and when a value is omitted we assume
it is null.
This commit is contained in:
jwilson
2017-04-20 08:37:37 -05:00
parent 81bbe870f1
commit 6112993919
2 changed files with 78 additions and 16 deletions

View File

@@ -72,13 +72,18 @@ internal class KotlinJsonAdapter<T> private constructor(
} }
reader.endObject() reader.endObject()
// Call the constructor using a Map so that absent optionals get defaults. // Confirm all parameters are present, optional, or nullable.
for (i in 0 until constructorSize) { for (i in 0 until constructorSize) {
if (!constructor.parameters[i].isOptional && values[i] === ABSENT_VALUE) { if (values[i] === ABSENT_VALUE && !constructor.parameters[i].isOptional) {
throw JsonDataException( if (!constructor.parameters[i].type.isMarkedNullable) {
"Required value ${constructor.parameters[i].name} missing at ${reader.path}") throw JsonDataException(
"Required value ${constructor.parameters[i].name} missing at ${reader.path}")
}
values[i] = null // Replace absent with null.
} }
} }
// Call the constructor using a Map so that absent optionals get defaults.
val result = constructor.callBy(IndexedParameterMap(constructor.parameters, values)) val result = constructor.callBy(IndexedParameterMap(constructor.parameters, values))
// Set remaining properties. // Set remaining properties.
@@ -109,7 +114,7 @@ internal class KotlinJsonAdapter<T> private constructor(
val parameter: KParameter?) { val parameter: KParameter?) {
init { init {
if (property !is KMutableProperty1 && parameter == null) { if (property !is KMutableProperty1 && parameter == null) {
throw IllegalArgumentException("No constructor or var property for ${property.name}") throw IllegalArgumentException("No constructor or var property for ${property}")
} }
} }
@@ -188,8 +193,7 @@ internal class KotlinJsonAdapter<T> private constructor(
for (parameter in constructor.parameters) { for (parameter in constructor.parameters) {
val binding = bindingsByName.remove(parameter.name) val binding = bindingsByName.remove(parameter.name)
if (binding == null && !parameter.isOptional) { if (binding == null && !parameter.isOptional) {
throw IllegalArgumentException( throw IllegalArgumentException("No property for required constructor ${parameter}")
"No property for required constructor parameter ${parameter.name}")
} }
bindings += binding bindings += binding
} }

View File

@@ -162,7 +162,6 @@ class KotlinJsonAdapterTest {
class ExplicitNull(var a: Int?, var b: Int?) class ExplicitNull(var a: Int?, var b: Int?)
// TODO(jwilson): if a nullable field is absent, just do the obvious thing instead of crashing?
@Test fun absentNull() { @Test fun absentNull() {
val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build()
val jsonAdapter = moshi.adapter(AbsentNull::class.java) val jsonAdapter = moshi.adapter(AbsentNull::class.java)
@@ -171,16 +170,27 @@ class KotlinJsonAdapterTest {
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"b\":5}") assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"b\":5}")
assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("{\"a\":null,\"b\":5}") assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("{\"a\":null,\"b\":5}")
try { val decoded = jsonAdapter.fromJson("{\"b\":6}")
jsonAdapter.fromJson("{\"b\":6}") assertThat(decoded.a).isNull()
fail() assertThat(decoded.b).isEqualTo(6)
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Required value a missing at $")
}
} }
class AbsentNull(var a: Int?, var b: Int?) class AbsentNull(var a: Int?, var b: Int?)
@Test fun repeatedValue() {
val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build()
val jsonAdapter = moshi.adapter(RepeatedValue::class.java)
try {
jsonAdapter.fromJson("{\"a\":4,\"b\":null,\"b\":6}")
fail()
} catch(expected: JsonDataException) {
assertThat(expected).hasMessage("Multiple values for b at $.b")
}
}
class RepeatedValue(var a: Int, var b: Int?)
@Test fun constructorParameterWithQualifier() { @Test fun constructorParameterWithQualifier() {
val moshi = Moshi.Builder() val moshi = Moshi.Builder()
.add(KotlinJsonAdapter.FACTORY) .add(KotlinJsonAdapter.FACTORY)
@@ -384,6 +394,26 @@ class KotlinJsonAdapterTest {
fun b() = b fun b() = b
} }
@Test fun privateConstructor() {
val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build()
val jsonAdapter = moshi.adapter(PrivateConstructor::class.java)
val encoded = PrivateConstructor.newInstance(3, 5)
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":3,\"b\":5}")
val decoded = jsonAdapter.fromJson("{\"a\":4,\"b\":6}")
assertThat(decoded.a()).isEqualTo(4)
assertThat(decoded.b()).isEqualTo(6)
}
class PrivateConstructor private constructor(var a: Int, var b: Int) {
fun a() = a
fun b() = b
companion object {
fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b)
}
}
@Test fun privateProperties() { @Test fun privateProperties() {
val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build()
val jsonAdapter = moshi.adapter(PrivateProperties::class.java) val jsonAdapter = moshi.adapter(PrivateProperties::class.java)
@@ -415,9 +445,37 @@ class KotlinJsonAdapterTest {
} }
} }
@Test fun unsettableProperty() {
val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build()
try {
moshi.adapter(UnsettableProperty::class.java)
fail()
} catch(expected: IllegalArgumentException) {
assertThat(expected).hasMessage("No constructor or var property for " +
"val ${UnsettableProperty::class.qualifiedName}.a: kotlin.Int")
}
}
class UnsettableProperty {
val a: Int = -1
var b: Int = -1
}
@Test fun nonPropertyConstructorParameter() {
val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build()
try {
moshi.adapter(NonPropertyConstructorParameter::class.java)
fail()
} catch(expected: IllegalArgumentException) {
assertThat(expected).hasMessage(
"No property for required constructor parameter #0 a of " + "fun <init>(" +
"kotlin.Int, kotlin.Int): ${NonPropertyConstructorParameter::class.qualifiedName}")
}
}
class NonPropertyConstructorParameter(a: Int, val b: Int)
// TODO(jwilson): resolve generic types? // TODO(jwilson): resolve generic types?
// TODO(jwilson): inaccessible constructors?
// TODO(jwilson): constructors parameter that is not a property
@Retention(RUNTIME) @Retention(RUNTIME)
@JsonQualifier @JsonQualifier