KotlinJsonAdapter (#281)

* Add kotlin-module with support for Kotlin data classes

* Naming and style changes to KotlinJsonAdapter.

Biggest changes:

 * Attempt to support regular classes and data classes
 * Avoid parameter hashing when indexing is sufficient for
   constructor parameters
This commit is contained in:
Jesse Wilson
2017-04-18 23:51:37 -04:00
committed by GitHub
parent 8c18caf574
commit 81bbe870f1
7 changed files with 778 additions and 27 deletions

89
kotlin/pom.xml Normal file
View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.5.0-SNAPSHOT</version>
</parent>
<artifactId>moshi-kotlin</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<out Annotation>
/**
* 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<T> private constructor(
val constructor: KFunction<T>,
val bindings: List<Binding<T, Any?>?>,
val options: JsonReader.Options) : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T {
val constructorSize = constructor.parameters.size
// Read each value into its slot in the array.
val values = Array<Any?>(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<K, P>(
val name: String,
val adapter: JsonAdapter<P>,
val property: KProperty1<K, P>,
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<K, P>).set(result, value)
}
}
}
/** A simple [Map] that uses parameter indexes instead of sorting or hashing. */
class IndexedParameterMap(val parameterKeys: List<KParameter>, val parameterValues: Array<Any?>)
: AbstractMap<KParameter, Any?>() {
override val entries: Set<Entry<KParameter, Any?>>
get() {
val allPossibleEntries = parameterKeys.mapIndexed { index, value ->
SimpleEntry<KParameter, Any?>(value, parameterValues[index])
}
return allPossibleEntries.filterTo(LinkedHashSet<Entry<KParameter, Any?>>()) {
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<String, Binding<Any, Any?>>()
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<Json>()
val parameter = parametersByName[property.name]
if (parameter != null) {
allAnnotations += parameter.annotations
if (jsonAnnotation == null) {
jsonAnnotation = parameter.findAnnotation<Json>()
}
}
val name = jsonAnnotation?.name ?: property.name
val adapter = moshi.adapter<Any>(
property.returnType.javaType, Util.jsonAnnotations(allAnnotations.toTypedArray()))
bindingsByName[property.name] =
Binding(name, adapter, property as KProperty1<Any, Any?>, parameter)
}
val bindings = ArrayList<Binding<Any, Any?>?>()
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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -105,19 +105,6 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
}
}
/**
* 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<T> extends JsonAdapter<T> {
}
};
/**
* 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<T> classFactory;
private final FieldBinding<?>[] fieldsArray;
private final JsonReader.Options options;

View File

@@ -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.
*
* <p>Although this annotation doesn't declare a {@link Target}, it is only honored in the following
* elements:
*
* <ul>
* <li><strong>Java class fields</strong>
* <li><strong>Kotlin properties</strong> for use with {@code moshi-kotlin}. This includes both
* properties declared in the constructor and properties declared as members.
* </ul>
*
* <p>Users of the <a href="https://github.com/rharter/auto-value-moshi">AutoValue: Moshi
* Extension</a> may also use this annotation on abstract getters.
*/
@Retention(RUNTIME)
@Documented
public @interface Json {

View File

@@ -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<? extends Annotation> annotation = Annotation.class;
Moshi.Builder builder = new Moshi.Builder();

25
pom.xml
View File

@@ -21,14 +21,16 @@
<module>moshi</module>
<module>examples</module>
<module>adapters</module>
<module>kotlin</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
<kotlin.version>1.1.1</kotlin.version>
<!-- Dependencies -->
<okio.version>1.11.0</okio.version>
<okio.version>1.12.0</okio.version>
<!-- Test Dependencies -->
<junit.version>4.12</junit.version>
@@ -71,6 +73,22 @@
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -100,6 +118,11 @@
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
</plugin>
</plugins>
</pluginManagement>