mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19:21 +08:00
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:
203
kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt
Normal file
203
kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapter.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
434
kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt
Normal file
434
kotlin/src/test/java/com/squareup/moshi/KotlinJsonAdapterTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user