Boolean properties with "is" prefix are misinterpreted during reflective DataFetcher generation
See original GitHub issueLibrary Version 1.4.2
Describe the bug
Declaring a Kotlin Boolean property with a prefix of “is” causes the default KotlinDataFetcherFactoryProvider
to return a graphql-java PropertyDataFetcher
with a name that does not resolve as expected.
Kotlin Boolean properties with an “is” prefix generate get methods without an additional prefix, e.g: val isFancy: Boolean
yields a generated method of boolean isFancy()
but PropertyDataFetcher
’s reflective method lookup only considers methods with an additional “is” or “get” prefix. This causes data fetching to fail at query time.
This results in additional unexpected behavior if Boolean properties isFancy
and fancy
are exposed on the same object (though this is admittedly a contrived scenario). PropertyDataFetcher
will resolve fancy
to point at the isFancy
property when queried, and attempting to fetch isFancy
will produce an error.
To Reproduce Steps to reproduce the behavior.
- Schema Configuration Default
- Kotlin code used to generate the schema
class Test {
val isFancy: Boolean get() = true
val fancy: Boolean get() = false
}
// Pass TopLevelObject(Query()) to toSchema()
class Query {
fun test() = Test()
}
Queries to reproduce:
# Cannot fetch the isFancy property
query LookupError {
test {
isFancy
}
}
# Fetches the isFancy property, not fancy (returns true rather than false)
query WrongProperty {
test {
fancy
}
}
Expected behavior The generated data fetchers fetch the Kotlin object properties with names matching the generated schema.
Workaround
Solving the issue completely may involve either upstream changes for graphql-java’s PropertyDataFetcher
or reimplementing it with Kotlin-aware behavior in graphql-kotlin to resolve the ambiguity between is-prefixed and unprefixed Boolean properties. The first issue illustrated can be worked around by replacing the KotlinDataFetcherFactoryProvider
as follows:
class FixedDataFetcherFactoryProvider(
hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks
) : KotlinDataFetcherFactoryProvider(hooks) {
@ExperimentalStdlibApi // for typeOf
override fun propertyDataFetcherFactory(
kClass: KClass<*>,
kProperty: KProperty<*>
) = DataFetcherFactory<Any> {
if (kProperty.name.startsWith("is")
&& kProperty.returnType == typeOf<Boolean>()
&& kProperty.name.length > 2
) {
// Omit "is" prefix that PropertyDataFetcher will add back
PropertyDataFetcher(kProperty.name.substring(2))
} else PropertyDataFetcher(kProperty.name)
}
}
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:5
Top GitHub Comments
While looking into this I’ve noticed that we actually don’t need to invoke the default
PropertyDataFetcher
fromgraphql-java
at all. While it is pretty well optimized and works great for retrieving fields from arbitrary POJO, it does so by using reflections to locate the target getter function. Since we are generating the schema using reflections, when we process the target property we already know which field on a class we have to access and can reference the target getter function directly. As a bonus, this also removes the ambiguity in resolving the field prefixed withis
as well.See https://github.com/ExpediaGroup/graphql-kotlin/pull/1018 for details
GraphQL Java maintainer here: in general we want to provide good interop with kotlin as long as it comes with reasonable costs. If this fix here would mean a bit tweaking of the PropertyDataFetcher I would recommend you open a PR and we can go from there.
In general I would recommend not to duplicated/copy the PropertyDataFetcher: it is quite optimized at this stage and I can imagine it will evolve further in the future.