From 954ca46b9ed994e6e52a9fc613b2dc80a9da4b98 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Mon, 8 Nov 2021 11:16:57 -0500 Subject: [PATCH] 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 --- .../moshi/adapters/EnumJsonAdapter.java | 5 +- .../squareup/moshi/recipes/FallbackEnum.java | 5 +- .../kotlin/codegen/api/TargetParameter.kt | 1 + .../kotlin/codegen/api/TargetProperty.kt | 3 +- .../moshi/kotlin/codegen/apt/metadata.kt | 50 ++++++-- .../moshi/kotlin/codegen/ksp/MoshiApiUtil.kt | 6 +- .../moshi/kotlin/codegen/ksp/TargetTypes.kt | 11 +- .../apt/JsonClassCodegenProcessorTest.kt | 22 +++- .../ksp/JsonClassSymbolProcessorTest.kt | 23 +++- .../moshi/kotlin/reflect/KotlinJsonAdapter.kt | 41 +++--- .../extra-moshi-test-module/build.gradle.kts | 4 + .../test/extra/AbstractClassInModuleA.kt | 14 ++- .../squareup/moshi/kotlin/DualKotlinTest.kt | 118 ++++++++++++++++++ .../kotlin/reflect/KotlinJsonAdapterTest.kt | 65 ++-------- .../com/squareup/moshi/ClassJsonAdapter.java | 8 +- .../main/java/com/squareup/moshi/Json.java | 19 ++- .../squareup/moshi/StandardJsonAdapters.java | 5 +- .../com/squareup/moshi/RecordJsonAdapter.java | 5 +- .../squareup/moshi/ClassJsonAdapterTest.java | 20 +++ 19 files changed, 317 insertions(+), 108 deletions(-) diff --git a/adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.java b/adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.java index bcbf594..215b583 100644 --- a/adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.java +++ b/adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.java @@ -68,7 +68,10 @@ public final class EnumJsonAdapter> extends JsonAdapter { for (int i = 0; i < constants.length; i++) { String constantName = constants[i].name(); 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; } options = JsonReader.Options.of(nameStrings); diff --git a/examples/src/main/java/com/squareup/moshi/recipes/FallbackEnum.java b/examples/src/main/java/com/squareup/moshi/recipes/FallbackEnum.java index 9edf0ac..c055761 100644 --- a/examples/src/main/java/com/squareup/moshi/recipes/FallbackEnum.java +++ b/examples/src/main/java/com/squareup/moshi/recipes/FallbackEnum.java @@ -79,7 +79,10 @@ final class FallbackEnum { for (int i = 0; i < constants.length; i++) { T constant = constants[i]; 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; } options = JsonReader.Options.of(nameStrings); diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt index 7a97b93..cdb1f18 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt @@ -26,5 +26,6 @@ public data class TargetParameter( val type: TypeName, val hasDefault: Boolean, val jsonName: String? = null, + val jsonIgnore: Boolean = false, val qualifiers: Set? = null ) diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt index d8752ec..3f86eb4 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt @@ -25,7 +25,8 @@ public data class TargetProperty( val propertySpec: PropertySpec, val parameter: TargetParameter?, val visibility: KModifier, - val jsonName: String? + val jsonName: String?, + val jsonIgnore: Boolean ) { val name: String get() = propertySpec.name val type: TypeName get() = propertySpec.type diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt index 40e20e8..2be304b 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/apt/metadata.kt @@ -64,6 +64,7 @@ import javax.tools.Diagnostic.Kind.WARNING private val JSON_QUALIFIER = JsonQualifier::class.java private val JSON = Json::class.asClassName() +private val TRANSIENT = Transient::class.asClassName() private val OBJECT_CLASS = ClassName("java.lang", "Object") private val VISIBILITY_MODIFIERS = setOf( KModifier.INTERNAL, @@ -95,7 +96,8 @@ internal fun primaryConstructor( type = parameter.type, hasDefault = parameter.defaultValue != null, 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, resolvedTypes: List ): Map { - val result = mutableMapOf() for (initialProperty in kotlinApi.propertySpecs) { val resolvedType = resolveTypeArgs( @@ -398,19 +399,22 @@ private fun declaredProperties( val property = initialProperty.toBuilder(type = resolvedType).build() val name = property.name val parameter = constructor.parameters[name] + val isIgnored = property.annotations.any { it.typeName == TRANSIENT } || + parameter?.jsonIgnore == true || + property.annotations.jsonIgnore() result[name] = TargetProperty( propertySpec = property, parameter = parameter, visibility = property.modifiers.visibility(), jsonName = parameter?.jsonName ?: property.annotations.jsonName() - ?: name.escapeDollarSigns() + ?: name.escapeDollarSigns(), + jsonIgnore = isIgnored ) } 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.isVisible: Boolean get() { @@ -429,11 +433,11 @@ internal fun TargetProperty.generator( elements: Elements, instantiateAnnotations: Boolean ): PropertyGenerator? { - if (isTransient) { + if (jsonIgnore) { if (!hasDefault) { messager.printMessage( ERROR, - "No default value for transient property $name", + "No default value for transient/ignored property $name", sourceElement ) return null @@ -510,16 +514,36 @@ private fun List?.qualifiers( private fun List?.jsonName(): String? { if (this == null) return null - return find { it.typeName == JSON }?.let { annotation -> - val mirror = requireNotNull(annotation.tag()) { - "Could not get the annotation mirror from the annotation spec" - } - mirror.elementValues.entries.single { - it.key.simpleName.contentEquals("name") - }.value.value as String + return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation -> + annotation.jsonName() } } +private fun List?.jsonIgnore(): Boolean { + if (this == null) return false + return filter { it.typeName == JSON }.firstNotNullOfOrNull { annotation -> + annotation.jsonIgnore() + } ?: false +} + +private fun AnnotationSpec.jsonName(): String? { + return elementValue("name").takeUnless { it == Json.UNSET_NAME } +} + +private fun AnnotationSpec.jsonIgnore(): Boolean { + return elementValue("ignore") ?: false +} + +private fun AnnotationSpec.elementValue(name: String): T? { + val mirror = requireNotNull(tag()) { + "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 { return replace("\$", "\${\'\$\'}") } diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt index 12eb9ca..e8f29f7 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt @@ -21,14 +21,12 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSDeclaration import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.KModifier -import com.squareup.kotlinpoet.asClassName import com.squareup.moshi.JsonQualifier import com.squareup.moshi.kotlin.codegen.api.DelegateKey import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator import com.squareup.moshi.kotlin.codegen.api.TargetProperty 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.isVisible: Boolean get() { @@ -47,10 +45,10 @@ internal fun TargetProperty.generator( originalType: KSDeclaration, instantiateAnnotations: Boolean ): PropertyGenerator? { - if (isTransient) { + if (jsonIgnore) { if (!hasDefault) { logger.error( - "No default value for transient property $name", + "No default value for transient/ignored property $name", originalType ) return null diff --git a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt index e75eebd..e3bfafa 100644 --- a/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt +++ b/kotlin/codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt @@ -213,7 +213,11 @@ private fun KSAnnotated?.qualifiers(resolver: Resolver): Set { } private fun KSAnnotated?.jsonName(): String? { - return this?.findAnnotationWithType()?.name + return this?.findAnnotationWithType()?.name?.takeUnless { it == Json.UNSET_NAME } +} + +private fun KSAnnotated?.jsonIgnore(): Boolean { + return this?.findAnnotationWithType()?.ignore ?: false } private fun declaredProperties( @@ -223,7 +227,6 @@ private fun declaredProperties( resolver: Resolver, typeParameterResolver: TypeParameterResolver, ): Map { - val result = mutableMapOf() for (property in classDecl.getDeclaredProperties()) { val initialType = property.type.resolve() @@ -235,12 +238,14 @@ private fun declaredProperties( val propertySpec = property.toPropertySpec(resolver, resolvedType, typeParameterResolver) val name = propertySpec.name val parameter = constructor.parameters[name] + val isTransient = property.isAnnotationPresent(Transient::class) result[name] = TargetProperty( propertySpec = propertySpec, parameter = parameter, visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC, jsonName = parameter?.jsonName ?: property.jsonName() - ?: name.escapeDollarSigns() + ?: name.escapeDollarSigns(), + jsonIgnore = isTransient || parameter?.jsonIgnore == true || property.jsonIgnore() ) } diff --git a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt index af37f71..89fdad0 100644 --- a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt +++ b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/apt/JsonClassCodegenProcessorTest.kt @@ -334,7 +334,27 @@ class JsonClassCodegenProcessorTest( ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) 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" ) } diff --git a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt index 3b0e154..426b776 100644 --- a/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt +++ b/kotlin/codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt @@ -357,7 +357,28 @@ class JsonClassSymbolProcessorTest( ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) 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" ) } diff --git a/kotlin/reflect/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt b/kotlin/reflect/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt index f4a01ac..a86ad5c 100644 --- a/kotlin/reflect/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt +++ b/kotlin/reflect/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapter.kt @@ -57,7 +57,7 @@ private val ABSENT_VALUE = Any() internal class KotlinJsonAdapter( val constructor: KFunction, val allBindings: List?>, - val nonTransientBindings: List>, + val nonIgnoredBindings: List>, val options: JsonReader.Options ) : JsonAdapter() { @@ -74,7 +74,7 @@ internal class KotlinJsonAdapter( reader.skipValue() continue } - val binding = nonTransientBindings[index] + val binding = nonIgnoredBindings[index] val propertyIndex = binding.propertyIndex if (values[propertyIndex] !== ABSENT_VALUE) { @@ -235,11 +235,27 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory { for (property in rawTypeKotlin.memberProperties) { val parameter = parametersByName[property.name] + property.isAccessible = true + var jsonAnnotation = property.findAnnotation() + val allAnnotations = property.annotations.toMutableList() + + if (parameter != null) { + allAnnotations += parameter.annotations + if (jsonAnnotation == null) { + jsonAnnotation = parameter.findAnnotation() + } + } + if (Modifier.isTransient(property.javaField?.modifiers ?: 0)) { require(parameter == null || parameter.isOptional) { "No default value for transient constructor $parameter" } 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) { @@ -248,18 +264,7 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory { if (property !is KMutableProperty1 && parameter == null) continue - property.isAccessible = true - val allAnnotations = property.annotations.toMutableList() - var jsonAnnotation = property.findAnnotation() - - if (parameter != null) { - allAnnotations += parameter.annotations - if (jsonAnnotation == null) { - jsonAnnotation = parameter.findAnnotation() - } - } - - val name = jsonAnnotation?.name ?: property.name + val name = jsonAnnotation?.name?.takeUnless { it == Json.UNSET_NAME } ?: property.name val propertyType = when (val propertyTypeClassifier = property.returnType.classifier) { is KClass<*> -> { if (propertyTypeClassifier.isValue) { @@ -294,7 +299,7 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory { @Suppress("UNCHECKED_CAST") bindingsByName[property.name] = KotlinJsonAdapter.Binding( name, - jsonAnnotation?.name ?: name, + jsonAnnotation?.name?.takeUnless { it == Json.UNSET_NAME } ?: name, adapter, property as KProperty1, parameter, @@ -317,8 +322,8 @@ public class KotlinJsonAdapterFactory : JsonAdapter.Factory { bindings += bindingByName.value.copy(propertyIndex = index++) } - val nonTransientBindings = bindings.filterNotNull() - val options = JsonReader.Options.of(*nonTransientBindings.map { it.name }.toTypedArray()) - return KotlinJsonAdapter(constructor, bindings, nonTransientBindings, options).nullSafe() + val nonIgnoredBindings = bindings.filterNotNull() + val options = JsonReader.Options.of(*nonIgnoredBindings.map { it.name }.toTypedArray()) + return KotlinJsonAdapter(constructor, bindings, nonIgnoredBindings, options).nullSafe() } } diff --git a/kotlin/tests/extra-moshi-test-module/build.gradle.kts b/kotlin/tests/extra-moshi-test-module/build.gradle.kts index d5dcb46..a0b0305 100644 --- a/kotlin/tests/extra-moshi-test-module/build.gradle.kts +++ b/kotlin/tests/extra-moshi-test-module/build.gradle.kts @@ -17,3 +17,7 @@ plugins { kotlin("jvm") } + +dependencies { + implementation(project(":moshi")) +} diff --git a/kotlin/tests/extra-moshi-test-module/src/main/kotlin/com/squareup/moshi/kotlin/codegen/test/extra/AbstractClassInModuleA.kt b/kotlin/tests/extra-moshi-test-module/src/main/kotlin/com/squareup/moshi/kotlin/codegen/test/extra/AbstractClassInModuleA.kt index da8b4cf..87aba20 100644 --- a/kotlin/tests/extra-moshi-test-module/src/main/kotlin/com/squareup/moshi/kotlin/codegen/test/extra/AbstractClassInModuleA.kt +++ b/kotlin/tests/extra-moshi-test-module/src/main/kotlin/com/squareup/moshi/kotlin/codegen/test/extra/AbstractClassInModuleA.kt @@ -15,10 +15,14 @@ */ package com.squareup.moshi.kotlin.codegen.test.extra +import com.squareup.moshi.Json + public abstract class AbstractClassInModuleA { - // Transients to ensure processor sees them across module boundaries since @Transient is - // SOURCE-only - // TODO uncomment these when https://github.com/google/ksp/issues/710 is fixed -// @Transient private lateinit var lateinitTransient: String -// @Transient private var regularTransient: String = "regularTransient" + // Ignored to ensure processor sees them across module boundaries. + // @Transient doesn't work for this case because it's source-only and jvm modifiers aren't currently visible in KSP. + + // Note that we target the field because otherwise it is stored on the synthetic holder method for + // 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" } diff --git a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt index e4268b3..19295c0 100644 --- a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt +++ b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt @@ -625,6 +625,124 @@ class DualKotlinTest { data class IntersectionTypes( val value: E ) where E : Enum, E : IntersectionTypeInterface + + @Test fun transientConstructorParameter() { + val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val jsonAdapter = moshi.adapter() + + 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() + + 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() + + 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() + + 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 diff --git a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt index 3a0424b..bb6bf4e 100644 --- a/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt +++ b/kotlin/tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt @@ -278,35 +278,6 @@ class KotlinJsonAdapterTest { var b: Int = -1 } - @Test fun transientConstructorParameter() { - val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - val jsonAdapter = moshi.adapter() - - 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() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { @@ -323,34 +294,22 @@ class KotlinJsonAdapterTest { class RequiredTransientConstructorParameter(@Transient var a: Int) - @Test fun transientProperty() { + @Test fun requiredIgnoredConstructorParameterFails() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - val jsonAdapter = moshi.adapter() - - 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 + try { + moshi.adapter() + fail() + } catch (expected: IllegalArgumentException) { + assertThat(expected).hasMessageThat().isEqualTo( + "No default value for ignored constructor parameter #0 " + + "a of fun (kotlin.Int): " + + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.RequiredIgnoredConstructorParameter" + ) } } + class RequiredIgnoredConstructorParameter(@Json(ignore = true) var a: Int) + @Test fun constructorParametersAndPropertiesWithSameNamesMustHaveSameTypes() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { diff --git a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java index 723e02d..41e4f01 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java @@ -134,6 +134,8 @@ final class ClassJsonAdapter extends JsonAdapter { boolean platformType = Util.isPlatformType(rawType); for (Field field : rawType.getDeclaredFields()) { 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. Type fieldType = resolve(type, rawType, field.getGenericType()); @@ -145,8 +147,10 @@ final class ClassJsonAdapter extends JsonAdapter { field.setAccessible(true); // 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 = jsonAnnotation != null ? jsonAnnotation.name() : fieldName; + String name = + jsonAnnotation != null && !Json.UNSET_NAME.equals(jsonAnnotation.name()) + ? jsonAnnotation.name() + : fieldName; FieldBinding fieldBinding = new FieldBinding<>(name, field, adapter); FieldBinding replaced = fieldBindings.put(name, fieldBinding); if (replaced != null) { diff --git a/moshi/src/main/java/com/squareup/moshi/Json.java b/moshi/src/main/java/com/squareup/moshi/Json.java index c51c70e..1b5bf1a 100644 --- a/moshi/src/main/java/com/squareup/moshi/Json.java +++ b/moshi/src/main/java/com/squareup/moshi/Json.java @@ -29,8 +29,9 @@ import java.lang.annotation.Target; * *
    *
  • Java class fields - *
  • Kotlin properties for use with {@code moshi-kotlin}. This includes both - * properties declared in the constructor and properties declared as members. + *
  • Kotlin properties for use with {@code moshi-kotlin} or {@code + * moshi-kotlin-codegen}. This includes both properties declared in the constructor and + * properties declared as members. *
* *

Users of the AutoValue: Moshi @@ -39,5 +40,17 @@ import java.lang.annotation.Target; @Retention(RUNTIME) @Documented 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. + * + *

Note: this has no effect in enums or record classes. + */ + boolean ignore() default false; } diff --git a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java index 124a711..158a371 100644 --- a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java +++ b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java @@ -275,7 +275,10 @@ final class StandardJsonAdapters { for (int i = 0; i < constants.length; i++) { T constant = constants[i]; 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; } options = JsonReader.Options.of(nameStrings); diff --git a/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java b/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java index af52783..6152d10 100644 --- a/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java +++ b/moshi/src/main/java16/com/squareup/moshi/RecordJsonAdapter.java @@ -91,7 +91,10 @@ final class RecordJsonAdapter extends JsonAdapter { Set qualifiers = null; for (var annotation : component.getDeclaredAnnotations()) { if (annotation instanceof Json jsonAnnotation) { - jsonName = jsonAnnotation.name(); + var annotationName = jsonAnnotation.name(); + if (!Json.UNSET_NAME.equals(annotationName)) { + jsonName = jsonAnnotation.name(); + } } else { if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) { if (qualifiers == null) { diff --git a/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java index 9e2f9b2..c17fde8 100644 --- a/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java @@ -153,6 +153,26 @@ public final class ClassJsonAdapterTest { 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 { int a; }