Add @Json.ignore (#1417)

* Default Json.name to an unset value

* Promote shared transient tests to DualKotlinTest

* Add new ignore property to Json

* Support it in ClassJsonAdapter

* Mention no enum/record support

* Support in KotlinJsonAdapter

* Rework code gen API to know of "ignored"

* Support in apt code gen

* Support in KSP

* Update old non-working transient example test

* Synthetic holders

* Use field on both
This commit is contained in:
Zac Sweers
2021-11-08 11:16:57 -05:00
committed by GitHub
parent 48e6dd3f03
commit 954ca46b9e
19 changed files with 317 additions and 108 deletions

View File

@@ -68,7 +68,10 @@ public final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
for (int i = 0; i < constants.length; i++) { for (int i = 0; i < constants.length; i++) {
String constantName = constants[i].name(); String constantName = constants[i].name();
Json annotation = enumType.getField(constantName).getAnnotation(Json.class); Json annotation = enumType.getField(constantName).getAnnotation(Json.class);
String name = annotation != null ? annotation.name() : constantName; String name =
annotation != null && !Json.UNSET_NAME.equals(annotation.name())
? annotation.name()
: constantName;
nameStrings[i] = name; nameStrings[i] = name;
} }
options = JsonReader.Options.of(nameStrings); options = JsonReader.Options.of(nameStrings);

View File

@@ -79,7 +79,10 @@ final class FallbackEnum {
for (int i = 0; i < constants.length; i++) { for (int i = 0; i < constants.length; i++) {
T constant = constants[i]; T constant = constants[i];
Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class); Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class);
String name = annotation != null ? annotation.name() : constant.name(); String name =
annotation != null && !Json.UNSET_NAME.equals(annotation.name())
? annotation.name()
: constant.name();
nameStrings[i] = name; nameStrings[i] = name;
} }
options = JsonReader.Options.of(nameStrings); options = JsonReader.Options.of(nameStrings);

View File

@@ -26,5 +26,6 @@ public data class TargetParameter(
val type: TypeName, val type: TypeName,
val hasDefault: Boolean, val hasDefault: Boolean,
val jsonName: String? = null, val jsonName: String? = null,
val jsonIgnore: Boolean = false,
val qualifiers: Set<AnnotationSpec>? = null val qualifiers: Set<AnnotationSpec>? = null
) )

View File

@@ -25,7 +25,8 @@ public data class TargetProperty(
val propertySpec: PropertySpec, val propertySpec: PropertySpec,
val parameter: TargetParameter?, val parameter: TargetParameter?,
val visibility: KModifier, val visibility: KModifier,
val jsonName: String? val jsonName: String?,
val jsonIgnore: Boolean
) { ) {
val name: String get() = propertySpec.name val name: String get() = propertySpec.name
val type: TypeName get() = propertySpec.type val type: TypeName get() = propertySpec.type

View File

@@ -64,6 +64,7 @@ import javax.tools.Diagnostic.Kind.WARNING
private val JSON_QUALIFIER = JsonQualifier::class.java private val JSON_QUALIFIER = JsonQualifier::class.java
private val JSON = Json::class.asClassName() private val JSON = Json::class.asClassName()
private val TRANSIENT = Transient::class.asClassName()
private val OBJECT_CLASS = ClassName("java.lang", "Object") private val OBJECT_CLASS = ClassName("java.lang", "Object")
private val VISIBILITY_MODIFIERS = setOf( private val VISIBILITY_MODIFIERS = setOf(
KModifier.INTERNAL, KModifier.INTERNAL,
@@ -95,7 +96,8 @@ internal fun primaryConstructor(
type = parameter.type, type = parameter.type,
hasDefault = parameter.defaultValue != null, hasDefault = parameter.defaultValue != null,
qualifiers = parameter.annotations.qualifiers(messager, elements), qualifiers = parameter.annotations.qualifiers(messager, elements),
jsonName = parameter.annotations.jsonName() jsonName = parameter.annotations.jsonName(),
jsonIgnore = parameter.annotations.jsonIgnore(),
) )
} }
@@ -386,7 +388,6 @@ private fun declaredProperties(
currentClass: ClassName, currentClass: ClassName,
resolvedTypes: List<ResolvedTypeMapping> resolvedTypes: List<ResolvedTypeMapping>
): Map<String, TargetProperty> { ): Map<String, TargetProperty> {
val result = mutableMapOf<String, TargetProperty>() val result = mutableMapOf<String, TargetProperty>()
for (initialProperty in kotlinApi.propertySpecs) { for (initialProperty in kotlinApi.propertySpecs) {
val resolvedType = resolveTypeArgs( val resolvedType = resolveTypeArgs(
@@ -398,19 +399,22 @@ private fun declaredProperties(
val property = initialProperty.toBuilder(type = resolvedType).build() val property = initialProperty.toBuilder(type = resolvedType).build()
val name = property.name val name = property.name
val parameter = constructor.parameters[name] val parameter = constructor.parameters[name]
val isIgnored = property.annotations.any { it.typeName == TRANSIENT } ||
parameter?.jsonIgnore == true ||
property.annotations.jsonIgnore()
result[name] = TargetProperty( result[name] = TargetProperty(
propertySpec = property, propertySpec = property,
parameter = parameter, parameter = parameter,
visibility = property.modifiers.visibility(), visibility = property.modifiers.visibility(),
jsonName = parameter?.jsonName ?: property.annotations.jsonName() jsonName = parameter?.jsonName ?: property.annotations.jsonName()
?: name.escapeDollarSigns() ?: name.escapeDollarSigns(),
jsonIgnore = isIgnored
) )
} }
return result return result
} }
private val TargetProperty.isTransient get() = propertySpec.annotations.any { it.typeName == Transient::class.asClassName() }
private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null
private val TargetProperty.isVisible: Boolean private val TargetProperty.isVisible: Boolean
get() { get() {
@@ -429,11 +433,11 @@ internal fun TargetProperty.generator(
elements: Elements, elements: Elements,
instantiateAnnotations: Boolean instantiateAnnotations: Boolean
): PropertyGenerator? { ): PropertyGenerator? {
if (isTransient) { if (jsonIgnore) {
if (!hasDefault) { if (!hasDefault) {
messager.printMessage( messager.printMessage(
ERROR, ERROR,
"No default value for transient property $name", "No default value for transient/ignored property $name",
sourceElement sourceElement
) )
return null return null
@@ -510,16 +514,36 @@ private fun List<AnnotationSpec>?.qualifiers(
private fun List<AnnotationSpec>?.jsonName(): String? { private fun List<AnnotationSpec>?.jsonName(): String? {
if (this == null) return null if (this == null) return null
return find { it.typeName == JSON }?.let { annotation -> return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation ->
val mirror = requireNotNull(annotation.tag<AnnotationMirror>()) { annotation.jsonName()
"Could not get the annotation mirror from the annotation spec"
}
mirror.elementValues.entries.single {
it.key.simpleName.contentEquals("name")
}.value.value as String
} }
} }
private fun List<AnnotationSpec>?.jsonIgnore(): Boolean {
if (this == null) return false
return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation ->
annotation.jsonIgnore()
} ?: false
}
private fun AnnotationSpec.jsonName(): String? {
return elementValue<String>("name").takeUnless { it == Json.UNSET_NAME }
}
private fun AnnotationSpec.jsonIgnore(): Boolean {
return elementValue<Boolean>("ignore") ?: false
}
private fun <T> AnnotationSpec.elementValue(name: String): T? {
val mirror = requireNotNull(tag<AnnotationMirror>()) {
"Could not get the annotation mirror from the annotation spec"
}
@Suppress("UNCHECKED_CAST")
return mirror.elementValues.entries.firstOrNull {
it.key.simpleName.contentEquals(name)
}?.value?.value as? T
}
private fun String.escapeDollarSigns(): String { private fun String.escapeDollarSigns(): String {
return replace("\$", "\${\'\$\'}") return replace("\$", "\${\'\$\'}")
} }

View File

@@ -21,14 +21,12 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSDeclaration
import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.asClassName
import com.squareup.moshi.JsonQualifier import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.kotlin.codegen.api.DelegateKey import com.squareup.moshi.kotlin.codegen.api.DelegateKey
import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
import com.squareup.moshi.kotlin.codegen.api.TargetProperty import com.squareup.moshi.kotlin.codegen.api.TargetProperty
import com.squareup.moshi.kotlin.codegen.api.rawType import com.squareup.moshi.kotlin.codegen.api.rawType
private val TargetProperty.isTransient get() = propertySpec.annotations.any { it.typeName == Transient::class.asClassName() }
private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null
private val TargetProperty.isVisible: Boolean private val TargetProperty.isVisible: Boolean
get() { get() {
@@ -47,10 +45,10 @@ internal fun TargetProperty.generator(
originalType: KSDeclaration, originalType: KSDeclaration,
instantiateAnnotations: Boolean instantiateAnnotations: Boolean
): PropertyGenerator? { ): PropertyGenerator? {
if (isTransient) { if (jsonIgnore) {
if (!hasDefault) { if (!hasDefault) {
logger.error( logger.error(
"No default value for transient property $name", "No default value for transient/ignored property $name",
originalType originalType
) )
return null return null

View File

@@ -213,7 +213,11 @@ private fun KSAnnotated?.qualifiers(resolver: Resolver): Set<AnnotationSpec> {
} }
private fun KSAnnotated?.jsonName(): String? { private fun KSAnnotated?.jsonName(): String? {
return this?.findAnnotationWithType<Json>()?.name return this?.findAnnotationWithType<Json>()?.name?.takeUnless { it == Json.UNSET_NAME }
}
private fun KSAnnotated?.jsonIgnore(): Boolean {
return this?.findAnnotationWithType<Json>()?.ignore ?: false
} }
private fun declaredProperties( private fun declaredProperties(
@@ -223,7 +227,6 @@ private fun declaredProperties(
resolver: Resolver, resolver: Resolver,
typeParameterResolver: TypeParameterResolver, typeParameterResolver: TypeParameterResolver,
): Map<String, TargetProperty> { ): Map<String, TargetProperty> {
val result = mutableMapOf<String, TargetProperty>() val result = mutableMapOf<String, TargetProperty>()
for (property in classDecl.getDeclaredProperties()) { for (property in classDecl.getDeclaredProperties()) {
val initialType = property.type.resolve() val initialType = property.type.resolve()
@@ -235,12 +238,14 @@ private fun declaredProperties(
val propertySpec = property.toPropertySpec(resolver, resolvedType, typeParameterResolver) val propertySpec = property.toPropertySpec(resolver, resolvedType, typeParameterResolver)
val name = propertySpec.name val name = propertySpec.name
val parameter = constructor.parameters[name] val parameter = constructor.parameters[name]
val isTransient = property.isAnnotationPresent(Transient::class)
result[name] = TargetProperty( result[name] = TargetProperty(
propertySpec = propertySpec, propertySpec = propertySpec,
parameter = parameter, parameter = parameter,
visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC, visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC,
jsonName = parameter?.jsonName ?: property.jsonName() jsonName = parameter?.jsonName ?: property.jsonName()
?: name.escapeDollarSigns() ?: name.escapeDollarSigns(),
jsonIgnore = isTransient || parameter?.jsonIgnore == true || property.jsonIgnore()
) )
} }

View File

@@ -334,7 +334,27 @@ class JsonClassCodegenProcessorTest(
) )
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains( assertThat(result.messages).contains(
"error: No default value for transient property a" "error: No default value for transient/ignored property a"
)
}
@Test
fun requiredIgnoredConstructorParameterFails() {
val result = compile(
kotlin(
"source.kt",
"""
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class RequiredIgnoredConstructorParameter(@Json(ignore = true) var a: Int)
"""
)
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"error: No default value for transient/ignored property a"
) )
} }

View File

@@ -357,7 +357,28 @@ class JsonClassSymbolProcessorTest(
) )
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains( assertThat(result.messages).contains(
"No default value for transient property a" "No default value for transient/ignored property a"
)
}
@Test
fun requiredIgnoredConstructorParameterFails() {
val result = compile(
kotlin(
"source.kt",
"""
package test
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class RequiredTransientConstructorParameter(@Json(ignore = true) var a: Int)
"""
)
)
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR)
assertThat(result.messages).contains(
"No default value for transient/ignored property a"
) )
} }

View File

@@ -57,7 +57,7 @@ private val ABSENT_VALUE = Any()
internal class KotlinJsonAdapter<T>( internal class KotlinJsonAdapter<T>(
val constructor: KFunction<T>, val constructor: KFunction<T>,
val allBindings: List<Binding<T, Any?>?>, val allBindings: List<Binding<T, Any?>?>,
val nonTransientBindings: List<Binding<T, Any?>>, val nonIgnoredBindings: List<Binding<T, Any?>>,
val options: JsonReader.Options val options: JsonReader.Options
) : JsonAdapter<T>() { ) : JsonAdapter<T>() {
@@ -74,7 +74,7 @@ internal class KotlinJsonAdapter<T>(
reader.skipValue() reader.skipValue()
continue continue
} }
val binding = nonTransientBindings[index] val binding = nonIgnoredBindings[index]
val propertyIndex = binding.propertyIndex val propertyIndex = binding.propertyIndex
if (values[propertyIndex] !== ABSENT_VALUE) { if (values[propertyIndex] !== ABSENT_VALUE) {
@@ -235,11 +235,27 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory {
for (property in rawTypeKotlin.memberProperties) { for (property in rawTypeKotlin.memberProperties) {
val parameter = parametersByName[property.name] val parameter = parametersByName[property.name]
property.isAccessible = true
var jsonAnnotation = property.findAnnotation<Json>()
val allAnnotations = property.annotations.toMutableList()
if (parameter != null) {
allAnnotations += parameter.annotations
if (jsonAnnotation == null) {
jsonAnnotation = parameter.findAnnotation()
}
}
if (Modifier.isTransient(property.javaField?.modifiers ?: 0)) { if (Modifier.isTransient(property.javaField?.modifiers ?: 0)) {
require(parameter == null || parameter.isOptional) { require(parameter == null || parameter.isOptional) {
"No default value for transient constructor $parameter" "No default value for transient constructor $parameter"
} }
continue continue
} else if (jsonAnnotation?.ignore == true) {
require(parameter == null || parameter.isOptional) {
"No default value for ignored constructor $parameter"
}
continue
} }
require(parameter == null || parameter.type == property.returnType) { require(parameter == null || parameter.type == property.returnType) {
@@ -248,18 +264,7 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory {
if (property !is KMutableProperty1 && parameter == null) continue if (property !is KMutableProperty1 && parameter == null) continue
property.isAccessible = true val name = jsonAnnotation?.name?.takeUnless { it == Json.UNSET_NAME } ?: property.name
val allAnnotations = property.annotations.toMutableList()
var jsonAnnotation = property.findAnnotation<Json>()
if (parameter != null) {
allAnnotations += parameter.annotations
if (jsonAnnotation == null) {
jsonAnnotation = parameter.findAnnotation()
}
}
val name = jsonAnnotation?.name ?: property.name
val propertyType = when (val propertyTypeClassifier = property.returnType.classifier) { val propertyType = when (val propertyTypeClassifier = property.returnType.classifier) {
is KClass<*> -> { is KClass<*> -> {
if (propertyTypeClassifier.isValue) { if (propertyTypeClassifier.isValue) {
@@ -294,7 +299,7 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
bindingsByName[property.name] = KotlinJsonAdapter.Binding( bindingsByName[property.name] = KotlinJsonAdapter.Binding(
name, name,
jsonAnnotation?.name ?: name, jsonAnnotation?.name?.takeUnless { it == Json.UNSET_NAME } ?: name,
adapter, adapter,
property as KProperty1<Any, Any?>, property as KProperty1<Any, Any?>,
parameter, parameter,
@@ -317,8 +322,8 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory {
bindings += bindingByName.value.copy(propertyIndex = index++) bindings += bindingByName.value.copy(propertyIndex = index++)
} }
val nonTransientBindings = bindings.filterNotNull() val nonIgnoredBindings = bindings.filterNotNull()
val options = JsonReader.Options.of(*nonTransientBindings.map { it.name }.toTypedArray()) val options = JsonReader.Options.of(*nonIgnoredBindings.map { it.name }.toTypedArray())
return KotlinJsonAdapter(constructor, bindings, nonTransientBindings, options).nullSafe() return KotlinJsonAdapter(constructor, bindings, nonIgnoredBindings, options).nullSafe()
} }
} }

View File

@@ -17,3 +17,7 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
} }
dependencies {
implementation(project(":moshi"))
}

View File

@@ -15,10 +15,14 @@
*/ */
package com.squareup.moshi.kotlin.codegen.test.extra package com.squareup.moshi.kotlin.codegen.test.extra
import com.squareup.moshi.Json
public abstract class AbstractClassInModuleA { public abstract class AbstractClassInModuleA {
// Transients to ensure processor sees them across module boundaries since @Transient is // Ignored to ensure processor sees them across module boundaries.
// SOURCE-only // @Transient doesn't work for this case because it's source-only and jvm modifiers aren't currently visible in KSP.
// TODO uncomment these when https://github.com/google/ksp/issues/710 is fixed
// @Transient private lateinit var lateinitTransient: String // Note that we target the field because otherwise it is stored on the synthetic holder method for
// @Transient private var regularTransient: String = "regularTransient" // annotations, which isn't visible from kapt
@field:Json(ignore = true) private lateinit var lateinitIgnored: String
@field:Json(ignore = true) private var regularIgnored: String = "regularIgnored"
} }

View File

@@ -625,6 +625,124 @@ class DualKotlinTest {
data class IntersectionTypes<E>( data class IntersectionTypes<E>(
val value: E val value: E
) where E : Enum<E>, E : IntersectionTypeInterface<E> ) where E : Enum<E>, E : IntersectionTypeInterface<E>
@Test fun transientConstructorParameter() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter<TransientConstructorParameter>()
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 multipleTransientConstructorParameters() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(MultipleTransientConstructorParameters::class.java)
val encoded = MultipleTransientConstructorParameters(3, 5, 7)
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)
assertThat(decoded.c).isEqualTo(-1)
}
class MultipleTransientConstructorParameters(@Transient var a: Int = -1, var b: Int = -1, @Transient var c: Int = -1)
@Test fun transientProperty() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter<TransientProperty>()
val encoded = TransientProperty()
encoded.a = 3
encoded.setB(4)
encoded.c = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"c":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":5,"c":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.getB()).isEqualTo(-1)
assertThat(decoded.c).isEqualTo(6)
}
class TransientProperty {
@Transient var a: Int = -1
@Transient private var b: Int = -1
var c: Int = -1
fun getB() = b
fun setB(b: Int) {
this.b = b
}
}
@Test fun ignoredConstructorParameter() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter<IgnoredConstructorParameter>()
val encoded = IgnoredConstructorParameter(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 IgnoredConstructorParameter(@Json(ignore = true) var a: Int = -1, var b: Int = -1)
@Test fun multipleIgnoredConstructorParameters() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(MultipleIgnoredConstructorParameters::class.java)
val encoded = MultipleIgnoredConstructorParameters(3, 5, 7)
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)
assertThat(decoded.c).isEqualTo(-1)
}
class MultipleIgnoredConstructorParameters(
@Json(ignore = true) var a: Int = -1,
var b: Int = -1,
@Json(ignore = true) var c: Int = -1
)
@Test fun ignoredProperty() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter<IgnoredProperty>()
val encoded = IgnoredProperty()
encoded.a = 3
encoded.setB(4)
encoded.c = 5
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"c":5}""")
val decoded = jsonAdapter.fromJson("""{"a":4,"b":5,"c":6}""")!!
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.getB()).isEqualTo(-1)
assertThat(decoded.c).isEqualTo(6)
}
class IgnoredProperty {
@Json(ignore = true) var a: Int = -1
@Json(ignore = true) private var b: Int = -1
var c: Int = -1
fun getB() = b
fun setB(b: Int) {
this.b = b
}
}
} }
typealias TypeAlias = Int typealias TypeAlias = Int

View File

@@ -278,35 +278,6 @@ class KotlinJsonAdapterTest {
var b: Int = -1 var b: Int = -1
} }
@Test fun transientConstructorParameter() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter<TransientConstructorParameter>()
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 multipleTransientConstructorParameters() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(MultipleTransientConstructorParameters::class.java)
val encoded = MultipleTransientConstructorParameters(3, 5, 7)
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)
assertThat(decoded.c).isEqualTo(-1)
}
class MultipleTransientConstructorParameters(@Transient var a: Int = -1, var b: Int = -1, @Transient var c: Int = -1)
@Test fun requiredTransientConstructorParameterFails() { @Test fun requiredTransientConstructorParameterFails() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
try { try {
@@ -323,34 +294,22 @@ class KotlinJsonAdapterTest {
class RequiredTransientConstructorParameter(@Transient var a: Int) class RequiredTransientConstructorParameter(@Transient var a: Int)
@Test fun transientProperty() { @Test fun requiredIgnoredConstructorParameterFails() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter<TransientProperty>() try {
moshi.adapter<RequiredIgnoredConstructorParameter>()
val encoded = TransientProperty() fail()
encoded.a = 3 } catch (expected: IllegalArgumentException) {
encoded.setB(4) assertThat(expected).hasMessageThat().isEqualTo(
encoded.c = 5 "No default value for ignored constructor parameter #0 " +
assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"c":5}""") "a of fun <init>(kotlin.Int): " +
"com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.RequiredIgnoredConstructorParameter"
val decoded = jsonAdapter.fromJson("""{"a":4,"b":5,"c":6}""")!! )
assertThat(decoded.a).isEqualTo(-1)
assertThat(decoded.getB()).isEqualTo(-1)
assertThat(decoded.c).isEqualTo(6)
}
class TransientProperty {
@Transient var a: Int = -1
@Transient private var b: Int = -1
var c: Int = -1
fun getB() = b
fun setB(b: Int) {
this.b = b
} }
} }
class RequiredIgnoredConstructorParameter(@Json(ignore = true) var a: Int)
@Test fun constructorParametersAndPropertiesWithSameNamesMustHaveSameTypes() { @Test fun constructorParametersAndPropertiesWithSameNamesMustHaveSameTypes() {
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
try { try {

View File

@@ -134,6 +134,8 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
boolean platformType = Util.isPlatformType(rawType); boolean platformType = Util.isPlatformType(rawType);
for (Field field : rawType.getDeclaredFields()) { for (Field field : rawType.getDeclaredFields()) {
if (!includeField(platformType, field.getModifiers())) continue; if (!includeField(platformType, field.getModifiers())) continue;
Json jsonAnnotation = field.getAnnotation(Json.class);
if (jsonAnnotation != null && jsonAnnotation.ignore()) continue;
// Look up a type adapter for this type. // Look up a type adapter for this type.
Type fieldType = resolve(type, rawType, field.getGenericType()); Type fieldType = resolve(type, rawType, field.getGenericType());
@@ -145,8 +147,10 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
field.setAccessible(true); field.setAccessible(true);
// Store it using the field's name. If there was already a field with this name, fail! // Store it using the field's name. If there was already a field with this name, fail!
Json jsonAnnotation = field.getAnnotation(Json.class); String name =
String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName; jsonAnnotation != null && !Json.UNSET_NAME.equals(jsonAnnotation.name())
? jsonAnnotation.name()
: fieldName;
FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter); FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding); FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
if (replaced != null) { if (replaced != null) {

View File

@@ -29,8 +29,9 @@ import java.lang.annotation.Target;
* *
* <ul> * <ul>
* <li><strong>Java class fields</strong> * <li><strong>Java class fields</strong>
* <li><strong>Kotlin properties</strong> for use with {@code moshi-kotlin}. This includes both * <li><strong>Kotlin properties</strong> for use with {@code moshi-kotlin} or {@code
* properties declared in the constructor and properties declared as members. * moshi-kotlin-codegen}. This includes both properties declared in the constructor and
* properties declared as members.
* </ul> * </ul>
* *
* <p>Users of the <a href="https://github.com/rharter/auto-value-moshi">AutoValue: Moshi * <p>Users of the <a href="https://github.com/rharter/auto-value-moshi">AutoValue: Moshi
@@ -39,5 +40,17 @@ import java.lang.annotation.Target;
@Retention(RUNTIME) @Retention(RUNTIME)
@Documented @Documented
public @interface Json { public @interface Json {
String name(); /** The default value of {@link #name()}. Should only be used to check if it's been set. */
String UNSET_NAME = "\u0000";
/** The name of the field when encoded as JSON. */
String name() default UNSET_NAME;
/**
* If true, this field/property will be ignored. This is semantically similar to use of {@code
* transient} on the JVM.
*
* <p><strong>Note:</strong> this has no effect in enums or record classes.
*/
boolean ignore() default false;
} }

View File

@@ -275,7 +275,10 @@ final class StandardJsonAdapters {
for (int i = 0; i < constants.length; i++) { for (int i = 0; i < constants.length; i++) {
T constant = constants[i]; T constant = constants[i];
Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class); Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class);
String name = annotation != null ? annotation.name() : constant.name(); String name =
annotation != null && !Json.UNSET_NAME.equals(annotation.name())
? annotation.name()
: constant.name();
nameStrings[i] = name; nameStrings[i] = name;
} }
options = JsonReader.Options.of(nameStrings); options = JsonReader.Options.of(nameStrings);

View File

@@ -91,7 +91,10 @@ final class RecordJsonAdapter<T> extends JsonAdapter<T> {
Set<Annotation> qualifiers = null; Set<Annotation> qualifiers = null;
for (var annotation : component.getDeclaredAnnotations()) { for (var annotation : component.getDeclaredAnnotations()) {
if (annotation instanceof Json jsonAnnotation) { if (annotation instanceof Json jsonAnnotation) {
jsonName = jsonAnnotation.name(); var annotationName = jsonAnnotation.name();
if (!Json.UNSET_NAME.equals(annotationName)) {
jsonName = jsonAnnotation.name();
}
} else { } else {
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) { if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
if (qualifiers == null) { if (qualifiers == null) {

View File

@@ -153,6 +153,26 @@ public final class ClassJsonAdapterTest {
assertThat(fromJson.b).isEqualTo(12); assertThat(fromJson.b).isEqualTo(12);
} }
static class IgnoredFields {
@Json(ignore = true)
int a;
int b;
}
@Test
public void ignoredFieldsOmitted() throws Exception {
IgnoredFields value = new IgnoredFields();
value.a = 11;
value.b = 12;
String toJson = toJson(IgnoredFields.class, value);
assertThat(toJson).isEqualTo("{\"b\":12}");
IgnoredFields fromJson = fromJson(IgnoredFields.class, "{\"a\":13,\"b\":12}");
assertThat(fromJson.a).isEqualTo(0); // Not assigned.
assertThat(fromJson.b).isEqualTo(12);
}
static class BaseA { static class BaseA {
int a; int a;
} }