diff --git a/kotlin/pom.xml b/kotlin/pom.xml new file mode 100644 index 0000000..8c50a16 --- /dev/null +++ b/kotlin/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + + com.squareup.moshi + moshi-parent + 1.5.0-SNAPSHOT + + + moshi-kotlin + + + + com.squareup.moshi + moshi + ${project.version} + + + junit + junit + test + + + org.assertj + assertj-core + test + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-test + test + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + + + diff --git a/kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt b/kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt new file mode 100644 index 0000000..cc8792a --- /dev/null +++ b/kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.moshi + +import java.lang.reflect.Modifier +import java.util.AbstractMap.SimpleEntry +import kotlin.collections.Map.Entry +import kotlin.reflect.KFunction +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.javaField +import kotlin.reflect.jvm.javaType + +/** Classes annotated with this are eligible for this adapter. */ +private val KOTLIN_METADATA = Class.forName("kotlin.Metadata") as Class + +/** + * Placeholder value used when a field is absent from the JSON. Note that this code + * distinguishes between absent values and present-but-null values. + */ +private object ABSENT_VALUE + +/** + * This class encodes Kotlin classes using their properties. It decodes them by first invoking the + * constructor, and then by setting any additional properties that exist, if any. + */ +internal class KotlinJsonAdapter private constructor( + val constructor: KFunction, + val bindings: List?>, + val options: JsonReader.Options) : JsonAdapter() { + + override fun fromJson(reader: JsonReader): T { + val constructorSize = constructor.parameters.size + + // Read each value into its slot in the array. + val values = Array(bindings.size) { ABSENT_VALUE } + reader.beginObject() + while (reader.hasNext()) { + val index = reader.selectName(options) + val binding = if (index != -1) bindings[index] else null + + if (binding == null) { + reader.nextName() + reader.skipValue() + continue + } + + if (values[index] !== ABSENT_VALUE) { + throw JsonDataException( + "Multiple values for ${constructor.parameters[index].name} at ${reader.path}") + } + + values[index] = binding.adapter.fromJson(reader) + } + reader.endObject() + + // Call the constructor using a Map so that absent optionals get defaults. + 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}") + } + } + val result = constructor.callBy(IndexedParameterMap(constructor.parameters, values)) + + // Set remaining properties. + for (i in constructorSize until bindings.size) { + bindings[i]!!.set(result, values[i]) + } + + return result + } + + override fun toJson(writer: JsonWriter, value: T) { + writer.beginObject() + for (binding in bindings) { + if (binding == null) continue // Skip constructor parameters that aren't properties. + + writer.name(binding.name) + binding.adapter.toJson(writer, binding.get(value)) + } + writer.endObject() + } + + override fun toString() = "KotlinJsonAdapter(${constructor.returnType})" + + data class Binding( + val name: String, + val adapter: JsonAdapter

, + val property: KProperty1, + val parameter: KParameter?) { + init { + if (property !is KMutableProperty1 && parameter == null) { + throw IllegalArgumentException("No constructor or var property for ${property.name}") + } + } + + fun get(value: K) = property.get(value) + + fun set(result: K, value: P) { + if (value !== ABSENT_VALUE) { + (property as KMutableProperty1).set(result, value) + } + } + } + + /** A simple [Map] that uses parameter indexes instead of sorting or hashing. */ + class IndexedParameterMap(val parameterKeys: List, val parameterValues: Array) + : AbstractMap() { + + override val entries: Set> + get() { + val allPossibleEntries = parameterKeys.mapIndexed { index, value -> + SimpleEntry(value, parameterValues[index]) + } + return allPossibleEntries.filterTo(LinkedHashSet>()) { + it.value !== ABSENT_VALUE + } + } + + override fun containsKey(key: KParameter) = parameterValues[key.index] !== ABSENT_VALUE + + override fun get(key: KParameter): Any? { + val value = parameterValues[key.index] + return if (value !== ABSENT_VALUE) value else null + } + } + + companion object { + @JvmField val FACTORY = Factory { type, annotations, moshi -> + if (!annotations.isEmpty()) return@Factory null + + val rawType = Types.getRawType(type) + val platformType = ClassJsonAdapter.isPlatformType(rawType) + if (platformType) return@Factory null + + if (!rawType.isAnnotationPresent(KOTLIN_METADATA)) return@Factory null + + val constructor = rawType.kotlin.primaryConstructor ?: return@Factory null + val parametersByName = constructor.parameters.associateBy { it.name } + constructor.isAccessible = true + + val bindingsByName = LinkedHashMap>() + + for (property in rawType.kotlin.memberProperties) { + if (Modifier.isTransient(property.javaField?.modifiers ?: 0)) continue + + property.isAccessible = true + var allAnnotations = property.annotations + var jsonAnnotation = property.findAnnotation() + + val parameter = parametersByName[property.name] + if (parameter != null) { + allAnnotations += parameter.annotations + if (jsonAnnotation == null) { + jsonAnnotation = parameter.findAnnotation() + } + } + + val name = jsonAnnotation?.name ?: property.name + val adapter = moshi.adapter( + property.returnType.javaType, Util.jsonAnnotations(allAnnotations.toTypedArray())) + + bindingsByName[property.name] = + Binding(name, adapter, property as KProperty1, parameter) + } + + val bindings = ArrayList?>() + + 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}") + } + bindings += binding + } + + bindings += bindingsByName.values + + val options = JsonReader.Options.of(*bindings.map { it?.name ?: "\u0000" }.toTypedArray()) + KotlinJsonAdapter(constructor, bindings, options) + } + } +} diff --git a/kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt b/kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt new file mode 100644 index 0000000..0c67c9a --- /dev/null +++ b/kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.moshi + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.fail +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.util.Locale +import java.util.SimpleTimeZone +import kotlin.annotation.AnnotationRetention.RUNTIME + +class KotlinJsonAdapterTest { + @Test fun constructorParameters() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ConstructorParameters::class.java) + + val encoded = ConstructorParameters(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 ConstructorParameters(var a: Int, var b: Int) + + @Test fun properties() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(Properties::class.java) + + val encoded = Properties() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":3,\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"a\":3,\"b\":5}") + assertThat(decoded.a).isEqualTo(3) + assertThat(decoded.b).isEqualTo(5) + } + + class Properties { + var a: Int = -1 + var b: Int = -1 + } + + @Test fun constructorParametersAndProperties() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ConstructorParametersAndProperties::class.java) + + val encoded = ConstructorParametersAndProperties(3) + encoded.b = 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 ConstructorParametersAndProperties(var a: Int) { + var b: Int = -1 + } + + @Test fun immutableConstructorParameters() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ImmutableConstructorParameters::class.java) + + val encoded = ImmutableConstructorParameters(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 ImmutableConstructorParameters(val a: Int, val b: Int) + + @Test fun immutableProperties() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ImmutableProperties::class.java) + + val encoded = ImmutableProperties(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":3,\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"a\":3,\"b\":5}") + assertThat(decoded.a).isEqualTo(3) + assertThat(decoded.b).isEqualTo(5) + } + + class ImmutableProperties(a: Int, b: Int) { + val a = a + val b = b + } + + @Test fun constructorDefaults() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ConstructorDefaultValues::class.java) + + val encoded = ConstructorDefaultValues(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":3,\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"b\":6}") + assertThat(decoded.a).isEqualTo(-1) + assertThat(decoded.b).isEqualTo(6) + } + + class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2) + + @Test fun requiredValueAbsent() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(RequiredValueAbsent::class.java) + + try { + jsonAdapter.fromJson("{\"a\":4}") + fail() + } catch(expected: JsonDataException) { + assertThat(expected).hasMessage("Required value b missing at $") + } + } + + class RequiredValueAbsent(var a: Int = 3, var b: Int) + + @Test fun duplicatedValue() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(DuplicateValue::class.java) + + try { + jsonAdapter.fromJson("{\"a\":4,\"a\":4}") + fail() + } catch(expected: JsonDataException) { + assertThat(expected).hasMessage("Multiple values for a at $.a") + } + } + + class DuplicateValue(var a: Int = -1, var b: Int = -2) + + @Test fun explicitNull() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ExplicitNull::class.java) + + val encoded = ExplicitNull(null, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"b\":5}") + assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("{\"a\":null,\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"a\":null,\"b\":6}") + assertThat(decoded.a).isEqualTo(null) + assertThat(decoded.b).isEqualTo(6) + } + + 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) + + val encoded = AbsentNull(null, 5) + 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 $") + } + } + + class AbsentNull(var a: Int?, var b: Int?) + + @Test fun constructorParameterWithQualifier() { + val moshi = Moshi.Builder() + .add(KotlinJsonAdapter.FACTORY) + .add(UppercaseJsonAdapter()) + .build() + val jsonAdapter = moshi.adapter(ConstructorParameterWithQualifier::class.java) + + val encoded = ConstructorParameterWithQualifier("Android", "Banana") + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":\"ANDROID\",\"b\":\"Banana\"}") + + val decoded = jsonAdapter.fromJson("{\"a\":\"Android\",\"b\":\"Banana\"}") + assertThat(decoded.a).isEqualTo("android") + assertThat(decoded.b).isEqualTo("Banana") + } + + class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String) + + @Test fun propertyWithQualifier() { + val moshi = Moshi.Builder() + .add(KotlinJsonAdapter.FACTORY) + .add(UppercaseJsonAdapter()) + .build() + val jsonAdapter = moshi.adapter(PropertyWithQualifier::class.java) + + val encoded = PropertyWithQualifier() + encoded.a = "Android" + encoded.b = "Banana" + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":\"ANDROID\",\"b\":\"Banana\"}") + + val decoded = jsonAdapter.fromJson("{\"a\":\"Android\",\"b\":\"Banana\"}") + assertThat(decoded.a).isEqualTo("android") + assertThat(decoded.b).isEqualTo("Banana") + } + + class PropertyWithQualifier { + @Uppercase var a: String = "" + var b: String = "" + } + + @Test fun constructorParameterWithJsonName() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ConstructorParameterWithJsonName::class.java) + + val encoded = ConstructorParameterWithJsonName(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"key a\":3,\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"key a\":4,\"b\":6}") + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int) + + @Test fun propertyWithJsonName() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(PropertyWithJsonName::class.java) + + val encoded = PropertyWithJsonName() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"key a\":3,\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"key a\":4,\"b\":6}") + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + class PropertyWithJsonName { + @Json(name = "key a") var a: Int = -1 + var b: Int = -1 + } + + @Test fun transientConstructorParameter() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(TransientConstructorParameter::class.java) + + val encoded = TransientConstructorParameter(3, 5) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"a\":4,\"b\":6}") + assertThat(decoded.a).isEqualTo(-1) + assertThat(decoded.b).isEqualTo(6) + } + + class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1) + + @Test fun transientProperty() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(TransientProperty::class.java) + + val encoded = TransientProperty() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"b\":5}") + + val decoded = jsonAdapter.fromJson("{\"a\":4,\"b\":6}") + assertThat(decoded.a).isEqualTo(-1) + assertThat(decoded.b).isEqualTo(6) + } + + class TransientProperty { + @Transient var a: Int = -1 + var b: Int = -1 + } + + @Test fun supertypeConstructorParameters() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(SubtypeConstructorParameters::class.java) + + val encoded = SubtypeConstructorParameters(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) + } + + open class SupertypeConstructorParameters(var a: Int) + + class SubtypeConstructorParameters(a: Int, var b: Int) : SupertypeConstructorParameters(a) + + @Test fun supertypeProperties() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(SubtypeProperties::class.java) + + val encoded = SubtypeProperties() + encoded.a = 3 + encoded.b = 5 + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"b\":5,\"a\":3}") + + val decoded = jsonAdapter.fromJson("{\"a\":4,\"b\":6}") + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.b).isEqualTo(6) + } + + open class SupertypeProperties { + var a: Int = -1 + } + + class SubtypeProperties : SupertypeProperties() { + var b: Int = -1 + } + + @Test fun extendsPlatformClassWithPrivateField() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ExtendsPlatformClassWithPrivateField::class.java) + + val encoded = ExtendsPlatformClassWithPrivateField(3) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":3}") + + val decoded = jsonAdapter.fromJson("{\"a\":4,\"id\":\"B\"}") + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.id).isEqualTo("C") + } + + internal class ExtendsPlatformClassWithPrivateField(var a: Int) : SimpleTimeZone(0, "C") + + @Test fun extendsPlatformClassWithProtectedField() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(ExtendsPlatformClassWithProtectedField::class.java) + + val encoded = ExtendsPlatformClassWithProtectedField(3) + assertThat(jsonAdapter.toJson(encoded)).isEqualTo("{\"a\":3,\"buf\":[0,0],\"count\":0}") + + val decoded = jsonAdapter.fromJson("{\"a\":4,\"buf\":[0,0],\"size\":0}") + assertThat(decoded.a).isEqualTo(4) + assertThat(decoded.buf()).isEqualTo(ByteArray(2, { 0 })) + assertThat(decoded.count()).isEqualTo(0) + } + + internal class ExtendsPlatformClassWithProtectedField(var a: Int) : ByteArrayOutputStream(2) { + fun buf() = buf + fun count() = count + } + + @Test fun platformTypeThrows() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + try { + moshi.adapter(Triple::class.java) + fail() + } catch (e: IllegalArgumentException) { + assertThat(e).hasMessage("Platform class kotlin.Triple annotated [] " + + "requires explicit JsonAdapter to be registered") + } + } + + @Test fun privateConstructorParameters() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(PrivateConstructorParameters::class.java) + + val encoded = PrivateConstructorParameters(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 PrivateConstructorParameters(private var a: Int, private var b: Int) { + fun a() = a + fun b() = b + } + + @Test fun privateProperties() { + val moshi = Moshi.Builder().add(KotlinJsonAdapter.FACTORY).build() + val jsonAdapter = moshi.adapter(PrivateProperties::class.java) + + val encoded = PrivateProperties() + encoded.a(3) + encoded.b(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 PrivateProperties { + var a: Int = -1 + var b: Int = -1 + + fun a() = a + + fun a(a: Int) { + this.a = a + } + + fun b() = b + + fun b(b: Int) { + this.b = b + } + } + + // TODO(jwilson): resolve generic types? + // TODO(jwilson): inaccessible constructors? + // TODO(jwilson): constructors parameter that is not a property + + @Retention(RUNTIME) + @JsonQualifier + annotation class Uppercase + + class UppercaseJsonAdapter { + @ToJson fun toJson(@Uppercase s: String) : String { + return s.toUpperCase(Locale.US) + } + @FromJson @Uppercase fun fromJson(s: String) : String { + return s.toLowerCase(Locale.US) + } + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java index 377efa9..e364bb6 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java @@ -105,19 +105,6 @@ final class ClassJsonAdapter extends JsonAdapter { } } - /** - * Returns true if {@code rawType} is built in. We don't reflect on private fields of platform - * types because they're unspecified and likely to be different on Java vs. Android. - */ - private boolean isPlatformType(Class rawType) { - String name = rawType.getName(); - return name.startsWith("android.") - || name.startsWith("java.") - || name.startsWith("javax.") - || name.startsWith("kotlin.") - || name.startsWith("scala."); - } - /** Returns true if fields with {@code modifiers} are included in the emitted JSON. */ private boolean includeField(boolean platformType, int modifiers) { if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false; @@ -125,6 +112,19 @@ final class ClassJsonAdapter extends JsonAdapter { } }; + /** + * Returns true if {@code rawType} is built in. We don't reflect on private fields of platform + * types because they're unspecified and likely to be different on Java vs. Android. + */ + static boolean isPlatformType(Class rawType) { + String name = rawType.getName(); + return name.startsWith("android.") + || name.startsWith("java.") + || name.startsWith("javax.") + || name.startsWith("kotlin.") + || name.startsWith("scala."); + } + private final ClassFactory classFactory; private final FieldBinding[] fieldsArray; private final JsonReader.Options options; diff --git a/moshi/src/main/java/com/squareup/moshi/Json.java b/moshi/src/main/java/com/squareup/moshi/Json.java index 908fa45..0da3be9 100644 --- a/moshi/src/main/java/com/squareup/moshi/Json.java +++ b/moshi/src/main/java/com/squareup/moshi/Json.java @@ -19,12 +19,23 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; -/** Customizes how a field is encoded as JSON. */ -@Target({FIELD, METHOD}) +/** + * Customizes how a field is encoded as JSON. + * + *

Although this annotation doesn't declare a {@link Target}, it is only honored in the following + * elements: + * + *

    + *
  • Java class fields + *
  • Kotlin properties for use with {@code moshi-kotlin}. This includes both + * properties declared in the constructor and properties declared as members. + *
+ * + *

Users of the AutoValue: Moshi + * Extension may also use this annotation on abstract getters. + */ @Retention(RUNTIME) @Documented public @interface Json { diff --git a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java index ccde79b..e0055c4 100644 --- a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java @@ -533,15 +533,6 @@ public final class MoshiTest { } @Test public void addNullFails() throws Exception { - JsonAdapter jsonAdapter = new JsonAdapter() { - @Override public Object fromJson(JsonReader reader) throws IOException { - throw new AssertionError(); - } - - @Override public void toJson(JsonWriter writer, Object value) throws IOException { - throw new AssertionError(); - } - }; Type type = Object.class; Class annotation = Annotation.class; Moshi.Builder builder = new Moshi.Builder(); diff --git a/pom.xml b/pom.xml index 09016e4..3f7af26 100644 --- a/pom.xml +++ b/pom.xml @@ -21,14 +21,16 @@ moshi examples adapters + kotlin UTF-8 1.7 + 1.1.1 - 1.11.0 + 1.12.0 4.12 @@ -71,6 +73,22 @@ assertj-core ${assertj.version} + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + @@ -100,6 +118,11 @@ + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} +