Repository: fasterxml/jackson-module-kotlin
Issue: Regression on data binding in 2.11.0 with specific field naming

Dependabot suggested an upgrade from Spring Boot 2.2.7 to 2.3.0, but the build failed; I checked it out and had the same failures. It looked like a Jackson issue, so I went back to Spring Boot 2.2.7 with Jackson 2.11.0, the version used in Spring Boot 2.3.0, and had the same problem.

I'm having this problem in a Kotlin project, but using Jackson-module-kotlin 2.11.0 alone doesn't seem to cause the problem.

I'm having the same problem in two places, the problem seems to be about the same in each case.  It's roughly something like this: I have an API request object that can come in multiple forms, say, an `owner` that can be a person or organization.  So `OwnerRequest` has an `organization` property, but it also has a private internal val `isOrganization` that returns a boolean if the `organization` field is set.

e.g.

```kotlin
data class OwnerRequest(val person: Person = null, val organization: Organization = null) {
    private val isOrganization = this.organization != null
}
```

I can see how there's a risk of confusion here, since Kotlin exposes getters and setters for Vals, and by java bean standards `is<Property>` could be a boolean getter for a a property. But in Jackson 2.10.4, all behaves as desired, and there hasn't been a problem.

However, if I replace 2.10.4 with 2.11.0, now there's a problem.  Suddenly, the server is responding as if `organization` wasn't set (triggering a validation failure).

As a reproduction, this test:
```kotlin
class JacksonTest {

	private val jackson = ObjectMapper()
		.findAndRegisterModules()

	@Test
	fun testOwnerDeserialization() {
		val value: OwnerRequest = jackson.readValue(
				"""
					{
						"org": "Wayne Industries"
					}
				""".trimIndent(),
				OwnerRequest::class.java
		)
		assertThat(value.org).isEqualTo("Wayne Industries")
		assertThat(value.toString()).isEqualTo("OwnerRequest(org=Wayne Industries, isOrg=true)")
	}
}

class OwnerRequest(val org: String? = null) {
	private val isOrg = org != null
	override fun toString(): String {
		return "OwnerRequest(org=$org, isOrg=$isOrg)"
	}
}
```
passes in 2.10.4 and fails in 2.11.0.

Here's the failure:
```
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "org" (class ca.cpp.api.submitapi.OwnerRequest), not marked as ignorable (one known property: "isOrg"])
 at [Source: (String)"{
	"org": "Wayne Industries"
}"; line: 3, column: 2] (through reference chain: ca.cpp.api.submitapi.OwnerRequest["org"])

	at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
	at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:855)
	at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:1206)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1592)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperties(BeanDeserializerBase.java:1542)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:511)
	at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1310)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:331)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4482)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3434)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3402)
	at ca.cpp.api.submitapi.JacksonTest.testOwnerDeserialization(JacksonTest.kt:23)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
```


Changed files:
src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt
src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt
src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/failing/Github340.kt

diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt
index 565487f..312f10d 100644
--- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt
+++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinNamesAnnotationIntrospector.kt
@@ -29,34 +29,34 @@ import kotlin.reflect.jvm.kotlinFunction
 
 internal class KotlinNamesAnnotationIntrospector(val module: KotlinModule, val cache: ReflectionCache, val ignoredClassesForImplyingJsonCreator: Set<KClass<*>>) : NopAnnotationIntrospector() {
     // since 2.4
-    override fun findImplicitPropertyName(member: AnnotatedMember): String? = when (member) {
-        is AnnotatedMethod -> if (member.name.contains('-') && member.parameterCount == 0) {
-            when {
-                member.name.startsWith("get") -> member.name.substringAfter("get")
-                member.name.startsWith("is") -> member.name.substringAfter("is")
-                else -> null
-            }?.replaceFirstChar { it.lowercase(Locale.getDefault()) }?.substringBefore('-')
-        } else null
-        is AnnotatedParameter -> findKotlinParameterName(member)
-        else -> null
-    }
-
-    // since 2.11: support Kotlin's way of handling "isXxx" backed properties where
-    // logical property name needs to remain "isXxx" and not become "xxx" as with Java Beans
-    // (see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html and
-    //  https://github.com/FasterXML/jackson-databind/issues/2527
-    //  for details)
-    override fun findRenameByField(config: MapperConfig<*>,
-                                   field: AnnotatedField,
-                                   implName: PropertyName): PropertyName? {
-        val origSimple = implName.simpleName
-        if (field.declaringClass.isKotlinClass() && origSimple.startsWith("is")) {
-            val mangledName: String? = BeanUtil.stdManglePropertyName(origSimple, 2)
-            if ((mangledName != null) && !mangledName.equals(origSimple)) {
-                return PropertyName.construct(mangledName)
-            }
+    override fun findImplicitPropertyName(member: AnnotatedMember): String? {
+        if (!member.declaringClass.isKotlinClass()) return null
+
+        val name = member.name
+
+        return when (member) {
+            is AnnotatedMethod -> if (member.parameterCount == 0) {
+                // The reason for truncating after `-` is to truncate the random suffix
+                // given after the value class accessor name.
+                when {
+                    name.startsWith("get") -> name.takeIf { it.contains("-") }?.let { _ ->
+                        name.substringAfter("get")
+                            .replaceFirstChar { it.lowercase(Locale.getDefault()) }
+                            .substringBefore('-')
+                    }
+                    // since 2.15: support Kotlin's way of handling "isXxx" backed properties where
+                    // logical property name needs to remain "isXxx" and not become "xxx" as with Java Beans
+                    // (see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html and
+                    //  https://github.com/FasterXML/jackson-databind/issues/2527 and
+                    //  https://github.com/FasterXML/jackson-module-kotlin/issues/340
+                    //  for details)
+                    name.startsWith("is") -> if (name.contains("-")) name.substringAfter("-") else name
+                    else -> null
+                }
+            } else null
+            is AnnotatedParameter -> findKotlinParameterName(member)
+            else -> null
         }
-        return null
     }
 
     @Suppress("UNCHECKED_CAST")
diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt
index b3dd29a..127b57b 100644
--- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt
+++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/ParameterNameTests.kt
@@ -25,24 +25,27 @@ class TestJacksonWithKotlin {
         val primaryAddress: String
         val wrongName: Boolean
         val createdDt: Date
+        val isName: Boolean
 
         fun validate(
             nameField: String = name,
             ageField: Int = age,
             addressField: String = primaryAddress,
             wrongNameField: Boolean = wrongName,
-            createDtField: Date = createdDt
+            createDtField: Date = createdDt,
+            isNameField: Boolean = isName,
         ) {
             assertThat(nameField, equalTo("Frank"))
             assertThat(ageField, equalTo(30))
             assertThat(addressField, equalTo("something here"))
             assertThat(wrongNameField, equalTo(true))
             assertThat(createDtField, equalTo(Date(1477419948000)))
+            assertThat(isNameField, equalTo(false))
         }
     }
 
-    private val normalCasedJson = """{"name":"Frank","age":30,"primaryAddress":"something here","renamed":true,"createdDt":"2016-10-25T18:25:48.000+00:00"}"""
-    private val pascalCasedJson = """{"Name":"Frank","Age":30,"PrimaryAddress":"something here","Renamed":true,"CreatedDt":"2016-10-25T18:25:48.000+00:00"}"""
+    private val normalCasedJson = """{"name":"Frank","age":30,"primaryAddress":"something here","renamed":true,"createdDt":"2016-10-25T18:25:48.000+00:00","isName":false}"""
+    private val pascalCasedJson = """{"Name":"Frank","Age":30,"PrimaryAddress":"something here","Renamed":true,"CreatedDt":"2016-10-25T18:25:48.000+00:00","IsName":false}"""
 
     private val normalCasedMapper = jacksonObjectMapper()
             .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
@@ -65,6 +68,7 @@ class TestJacksonWithKotlin {
 
         override var primaryAddress: String = ""
         override var createdDt: Date = Date()
+        override val isName: Boolean = false
     }
 
     @Test fun NoFailWithDefaultAndSpecificConstructor() {
@@ -79,7 +83,8 @@ class TestJacksonWithKotlin {
         override val age: Int,
         override val primaryAddress: String,
         val renamed: Boolean,
-        override val createdDt: Date
+        override val createdDt: Date,
+        override val isName: Boolean
     ) : TestFields {
         @JsonIgnore
         override val wrongName = renamed // here for the test validation only
@@ -97,7 +102,8 @@ class TestJacksonWithKotlin {
         override val age: Int,
         override val primaryAddress: String,
         val renamed: Boolean,
-        override val createdDt: Date
+        override val createdDt: Date,
+        override val isName: Boolean
     ) : TestFields {
         @JsonIgnore
         override val wrongName = renamed // here for the test validation only
@@ -121,7 +127,8 @@ class TestJacksonWithKotlin {
         override val age: Int,
         override val primaryAddress: String,
         @JsonProperty("renamed") override val wrongName: Boolean,
-        override val createdDt: Date
+        override val createdDt: Date,
+        override val isName: Boolean
     ) : TestFields
 
     @Test fun testDataClassWithExplicitJsonCreatorAndJsonProperty() {
@@ -141,7 +148,8 @@ class TestJacksonWithKotlin {
         override val age: Int,
         override val primaryAddress: String,
         @JsonProperty("renamed") override val wrongName: Boolean,
-        override val createdDt: Date
+        override val createdDt: Date,
+        override val isName: Boolean
     ) : TestFields
 
     @Test fun testNormalClassWithJsonCreator() {
@@ -155,7 +163,8 @@ class TestJacksonWithKotlin {
     private class StateObjectWithPartialFieldsInConstructor(
         override val name: String,
         override val age: Int,
-        override val primaryAddress: String
+        override val primaryAddress: String,
+        override val isName: Boolean
     ) : TestFields {
         @JsonProperty("renamed") override var wrongName: Boolean = false
         override var createdDt: Date by Delegates.notNull()
@@ -176,7 +185,8 @@ class TestJacksonWithKotlin {
         override val age: Int,
         override val primaryAddress: String,
         @JsonProperty("renamed") override val wrongName: Boolean,
-        override val createdDt: Date
+        override val createdDt: Date,
+        override val isName: Boolean
     ) : TestFields
 
     @Test fun testDataClassWithNonFieldParametersInConstructor() {
@@ -207,7 +217,8 @@ class TestJacksonWithKotlin {
         override val age: Int,
         override val primaryAddress: String,
         override val wrongName: Boolean,
-        override val createdDt: Date
+        override val createdDt: Date,
+        override val isName: Boolean
     ) : TestFields {
         var factoryUsed: Boolean = false
         companion object {
@@ -216,9 +227,10 @@ class TestJacksonWithKotlin {
                 @JsonProperty("age") age: Int,
                 @JsonProperty("primaryAddress") primaryAddress: String,
                 @JsonProperty("renamed") wrongName: Boolean,
-                @JsonProperty("createdDt") createdDt: Date
+                @JsonProperty("createdDt") createdDt: Date,
+                @JsonProperty("isName") isName: Boolean
             ): StateObjectWithFactory {
-                val obj = StateObjectWithFactory(nameThing, age, primaryAddress, wrongName, createdDt)
+                val obj = StateObjectWithFactory(nameThing, age, primaryAddress, wrongName, createdDt, isName)
                 obj.factoryUsed = true
                 return obj
             }
@@ -236,7 +248,8 @@ class TestJacksonWithKotlin {
         val age: Int,
         val primaryAddress: String,
         val renamed: Boolean,
-        val createdDt: Date
+        val createdDt: Date,
+        val isName: Boolean
     ) {
         companion object {
             @JvmStatic @JsonCreator fun create(
@@ -244,9 +257,10 @@ class TestJacksonWithKotlin {
                 age: Int,
                 primaryAddress: String,
                 renamed: Boolean,
-                createdDt: Date
+                createdDt: Date,
+                isName: Boolean
             ): StateObjectWithFactoryNoParamAnnotations {
-                return StateObjectWithFactoryNoParamAnnotations(name, age, primaryAddress, renamed, createdDt)
+                return StateObjectWithFactoryNoParamAnnotations(name, age, primaryAddress, renamed, createdDt, isName)
             }
         }
     }
@@ -266,7 +280,8 @@ class TestJacksonWithKotlin {
         override val age: Int,
         override val primaryAddress: String,
         override val wrongName: Boolean,
-        override val createdDt: Date
+        override val createdDt: Date,
+        override val isName: Boolean
     ) : TestFields {
         var factoryUsed: Boolean = false
         private companion object Named {
@@ -275,9 +290,10 @@ class TestJacksonWithKotlin {
                 @JsonProperty("age") age: Int,
                 @JsonProperty("primaryAddress") primaryAddress: String,
                 @JsonProperty("renamed") wrongName: Boolean,
-                @JsonProperty("createdDt") createdDt: Date
+                @JsonProperty("createdDt") createdDt: Date,
+                @JsonProperty("isName") isName: Boolean
             ): StateObjectWithFactoryOnNamedCompanion {
-                val obj = StateObjectWithFactoryOnNamedCompanion(nameThing, age, primaryAddress, wrongName, createdDt)
+                val obj = StateObjectWithFactoryOnNamedCompanion(nameThing, age, primaryAddress, wrongName, createdDt, isName)
                 obj.factoryUsed = true
                 return obj
             }
diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/failing/Github340.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github340.kt
similarity index 55%
rename from src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/failing/Github340.kt
rename to src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github340.kt
index dba6a07..7d38cbd 100644
--- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/failing/Github340.kt
+++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/test/github/Github340.kt
@@ -1,10 +1,9 @@
-package com.fasterxml.jackson.module.kotlin.test.github.failing
+package com.fasterxml.jackson.module.kotlin.test.github
 
 import com.fasterxml.jackson.databind.ObjectMapper
-import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import com.fasterxml.jackson.module.kotlin.kotlinModule
 import com.fasterxml.jackson.module.kotlin.readValue
-import com.fasterxml.jackson.module.kotlin.test.expectFailure
 import org.junit.Test
 import kotlin.test.assertEquals
 
@@ -21,10 +20,9 @@ class OwnerRequestTest {
 
     @Test
     fun testDeserHit340() {
-        expectFailure<UnrecognizedPropertyException>("GitHub #340 has been fixed!") {
-            val value: IsField = jackson.readValue(json)
-            assertEquals("Got a foo", value.foo)
-        }
+        val value: IsField = jackson.readValue(json)
+        // Fixed
+        assertEquals("Got a foo", value.foo)
     }
 
     @Test
@@ -32,4 +30,17 @@ class OwnerRequestTest {
         val value: NoIsField = jackson.readValue(json)
         assertEquals("Got a foo", value.foo)
     }
+
+    // A test case for isSetter to work, added with the fix for this issue.
+    class IsSetter {
+        lateinit var isFoo: String
+    }
+
+    @Test
+    fun isSetterTest() {
+        val json = """{"isFoo":"bar"}"""
+        val isSetter: IsSetter = jackson.readValue(json)
+
+        assertEquals("bar", isSetter.isFoo)
+    }
 }