diff --git a/kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt b/kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt index cc8792a..fe72818 100644 --- a/kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt +++ b/kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt @@ -72,13 +72,18 @@ internal class KotlinJsonAdapter private constructor( } 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) { - if (!constructor.parameters[i].isOptional && values[i] === ABSENT_VALUE) { - throw JsonDataException( - "Required value ${constructor.parameters[i].name} missing at ${reader.path}") + if (values[i] === ABSENT_VALUE && !constructor.parameters[i].isOptional) { + if (!constructor.parameters[i].type.isMarkedNullable) { + 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)) // Set remaining properties. @@ -109,7 +114,7 @@ internal class KotlinJsonAdapter private constructor( val parameter: KParameter?) { init { 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 private constructor( for (parameter in constructor.parameters) { val binding = bindingsByName.remove(parameter.name) if (binding == null && !parameter.isOptional) { - throw IllegalArgumentException( - "No property for required constructor parameter ${parameter.name}") + throw IllegalArgumentException("No property for required constructor ${parameter}") } bindings += binding } diff --git a/kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt b/kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt index 0c67c9a..781fb10 100644 --- a/kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt +++ b/kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt @@ -162,7 +162,6 @@ class KotlinJsonAdapterTest { 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() { val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() val jsonAdapter = moshi.adapter(AbsentNull::class.java) @@ -171,16 +170,27 @@ class KotlinJsonAdapterTest { assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"b\":5}") assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("{\"a\":null,\"b\":5}") - try { - jsonAdapter.fromJson("{\"b\":6}") - fail() - } catch(expected: JsonDataException) { - assertThat(expected).hasMessage("Required value a missing at $") - } + val decoded = jsonAdapter.fromJson("{\"b\":6}") + assertThat(decoded.a).isNull() + assertThat(decoded.b).isEqualTo(6) } 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() { val moshi = Moshi.Builder() .add(KotlinJsonAdapter.FACTORY) @@ -384,6 +394,26 @@ class KotlinJsonAdapterTest { 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() { val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() 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 (" + + "kotlin.Int, kotlin.Int): ${NonPropertyConstructorParameter::class.qualifiedName}") + } + } + + class NonPropertyConstructorParameter(a: Int, val b: Int) + // TODO(jwilson): resolve generic types? - // TODO(jwilson): inaccessible constructors? - // TODO(jwilson): constructors parameter that is not a property @Retention(RUNTIME) @JsonQualifier