Skip to content

Commit 2c19290

Browse files
authored
Merge pull request #931 from graphql-java-kickstart/bugfix/926-kotlin-empty-scalars
Keep the values of Kotlin custom scalars without visible properties in arguments
2 parents 35ff6ff + 91d25e2 commit 2c19290

2 files changed

Lines changed: 106 additions & 7 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/resolver/ArgumentConverter.kt‎

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import tools.jackson.core.JacksonException
44
import tools.jackson.core.JsonGenerator
55
import tools.jackson.core.JsonParser
66
import tools.jackson.core.JsonToken
7+
import tools.jackson.core.util.JsonGeneratorDelegate
78
import tools.jackson.databind.DeserializationContext
89
import tools.jackson.databind.DeserializationFeature
910
import tools.jackson.databind.JacksonSerializable
@@ -14,6 +15,7 @@ import tools.jackson.databind.SerializationFeature
1415
import tools.jackson.databind.deser.DeserializationProblemHandler
1516
import tools.jackson.databind.deser.ValueInstantiator
1617
import tools.jackson.databind.jsontype.TypeSerializer
18+
import tools.jackson.databind.ser.bean.BeanSerializerBase
1719
import tools.jackson.databind.util.TokenBuffer
1820
import java.lang.reflect.Type
1921

@@ -73,16 +75,17 @@ internal class ArgumentConverter(private val mapper: ObjectMapper, type: Type) {
7375
}
7476

7577
private fun convertsToOwnClass(value: Any): Boolean = try {
76-
mapper.readValue(buffer(value), value.javaClass)
78+
mapper.readValue(bufferFailingOnEmptyBeans(value), value.javaClass)
7779
true
7880
} catch (e: JacksonException) {
7981
false
8082
}
8183

82-
private fun read(value: Any?): Any? = reader.readValue(buffer(value))
84+
private fun read(value: Any?): Any? = reader.readValue(bufferFailingOnEmptyBeans(value))
8385

84-
private fun buffer(value: Any?): TokenBuffer =
85-
writer.writeValueIntoBuffer(value).forceUseOfBigDecimal(useBigDecimalForFloats)
86+
private fun bufferFailingOnEmptyBeans(value: Any?): TokenBuffer = buffer(FailOnEmptyBeans(value))
87+
88+
private fun buffer(value: Any?): TokenBuffer = writer.writeValueIntoBuffer(value).forceUseOfBigDecimal(useBigDecimalForFloats)
8689

8790
private val embeddedValueHandler = object : DeserializationProblemHandler() {
8891
// called for an interface or abstract class, e.g. Part, since no deserializer of it returns embedded values
@@ -93,13 +96,21 @@ internal class ArgumentConverter(private val mapper: ObjectMapper, type: Type) {
9396
p: JsonParser,
9497
msg: String?
9598
): Any? {
96-
val value = if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) p.embeddedObject else null
99+
val value = p.embeddedValue()
97100
return if (instClass.isInstance(value)) value else NOT_HANDLED
98101
}
99102

100103
// called for an embedded value read as a bean of another class, which the value is converted to on its own
101104
override fun handleWeirdNativeValue(context: DeserializationContext, targetType: JavaType, valueToConvert: Any, p: JsonParser): Any? =
102-
mapper.readValue(buffer(valueToConvert), targetType)
105+
convertOnItsOwn(valueToConvert, targetType)
106+
107+
// called for an embedded value read as a Map or JsonNode, e.g. a scalar without properties, which converts to {}
108+
override fun handleUnexpectedToken(context: DeserializationContext, targetType: JavaType, t: JsonToken?, p: JsonParser, failureMsg: String?): Any? =
109+
p.embeddedValue()?.let { convertOnItsOwn(it, targetType) } ?: NOT_HANDLED
110+
111+
private fun JsonParser.embeddedValue(): Any? = if (hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) embeddedObject else null
112+
113+
private fun convertOnItsOwn(value: Any, targetType: JavaType): Any? = mapper.readValue(buffer(value), targetType)
103114
}
104115

105116
// FAIL_ON_EMPTY_BEANS whatever the mapper says: a scalar class with no visible properties would otherwise be read back as
@@ -114,6 +125,32 @@ internal class ArgumentConverter(private val mapper: ObjectMapper, type: Type) {
114125

115126
private val nullToJavaDefaultReader by lazy { reader.without(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES) }
116127

128+
// FAIL_ON_EMPTY_BEANS skips a class with annotations, which Jackson writes as an empty object even if it has no properties,
129+
// e.g. any Kotlin class, since they all have @Metadata
130+
private class FailOnEmptyBeans(private val value: Any?) : JacksonSerializable {
131+
override fun serialize(gen: JsonGenerator, serializers: SerializationContext) {
132+
serializers.writeValue(EmptyBeanCheck(gen, serializers), value)
133+
}
134+
135+
override fun serializeWithType(gen: JsonGenerator, serializers: SerializationContext, typeSer: TypeSerializer) {
136+
serialize(gen, serializers)
137+
}
138+
}
139+
140+
private class EmptyBeanCheck(gen: JsonGenerator, private val serializers: SerializationContext) : JsonGeneratorDelegate(gen, false) {
141+
override fun writeEndObject(): JsonGenerator {
142+
val context = streamWriteContext()
143+
val bean = context.currentValue()
144+
if (context.entryCount == 0 && bean != null && hasNoProperties(bean.javaClass)) {
145+
serializers.reportBadDefinition<Unit>(bean.javaClass, "No properties discovered to write ${bean.javaClass.name}")
146+
}
147+
return super.writeEndObject()
148+
}
149+
150+
private fun hasNoProperties(beanClass: Class<*>) =
151+
(serializers.findValueSerializer(beanClass) as? BeanSerializerBase)?.properties()?.hasNext() == false
152+
}
153+
117154
private class EmbeddedValue(private val value: Any) : JacksonSerializable {
118155
override fun serialize(gen: JsonGenerator, serializers: SerializationContext) {
119156
gen.writeEmbeddedObject(value)

‎src/test/kotlin/graphql/kickstart/tools/CustomScalarInputTest.kt‎

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,35 @@ class CustomScalarInputTest {
188188
assertEquals(data, mapOf("payment" to "kept"))
189189
}
190190

191+
@Test
192+
fun `Kotlin custom scalars without visible properties are passed`() {
193+
val data = assertNoGraphQlErrors(graphQL(inputSchema)) {
194+
"""
195+
query {
196+
token(token: "argument")
197+
optionalToken(token: "optional")
198+
tokenInput(input: { token: "input" })
199+
tokenNode(input: { token: "node" })
200+
}
201+
"""
202+
}
203+
204+
assertEquals(data, mapOf("token" to "argument", "optionalToken" to "optional", "tokenInput" to "input", "tokenNode" to "node"))
205+
}
206+
207+
@Test
208+
fun `Kotlin custom scalars without visible properties still convert to other classes`() {
209+
val data = assertNoGraphQlErrors(graphQL(inputSchema)) {
210+
"""
211+
query {
212+
otherTokens(input: { bean: "bean", map: "map", node: "node" })
213+
}
214+
"""
215+
}
216+
217+
assertEquals(data, mapOf("otherTokens" to "OtherToken(value=other) {} {}"))
218+
}
219+
191220
@Test
192221
fun `errors converting inputs with custom scalars name the actual problem`() {
193222
val result = graphQL(inputSchema).execute(
@@ -205,7 +234,7 @@ class CustomScalarInputTest {
205234
private fun graphQL(schema: String, options: SchemaParserOptions = SchemaParserOptions.defaultOptions()): GraphQL {
206235
val graphQLSchema = SchemaParser.newParser()
207236
.schemaString(schema)
208-
.scalars(localDateScalar, opaqueScalar, secretScalar, boxScalar, moneyScalar, customScalarUUID, uploadScalar)
237+
.scalars(localDateScalar, opaqueScalar, secretScalar, boxScalar, moneyScalar, tokenScalar, customScalarUUID, uploadScalar)
209238
.resolvers(resolver)
210239
.options(options)
211240
.build()
@@ -221,6 +250,7 @@ class CustomScalarInputTest {
221250
scalar Box
222251
scalar Money
223252
scalar Upload
253+
scalar Token
224254
225255
type Query {
226256
event(input: EventInput!): String!
@@ -232,6 +262,11 @@ class CustomScalarInputTest {
232262
priceNode(input: NodeInput!): String!
233263
payment(input: PaymentInput!): String!
234264
price(price: Money!): String!
265+
token(token: Token!): String!
266+
optionalToken(token: Token): String!
267+
tokenInput(input: TokenInput!): String!
268+
tokenNode(input: NodeInput!): String!
269+
otherTokens(input: OtherTokensInput!): String!
235270
}
236271
237272
input EventInput {
@@ -255,6 +290,17 @@ class CustomScalarInputTest {
255290
input NodeInput {
256291
secret: Secret
257292
price: Money
293+
token: Token
294+
}
295+
296+
input TokenInput {
297+
token: Token
298+
}
299+
300+
input OtherTokensInput {
301+
bean: Token
302+
map: Token
303+
node: Token
258304
}
259305
260306
input PaymentInput {
@@ -290,6 +336,11 @@ class CustomScalarInputTest {
290336
fun priceNode(input: ObjectNode): String = input.toString()
291337
fun payment(input: PaymentInput): String = listOfNotNull(input.price, input.reference, input.secret, input.box).joinToString(" ")
292338
fun price(price: MoneyDto): String = price.toString()
339+
fun token(token: Token): String = token.toString()
340+
fun optionalToken(token: Optional<Token>): String = token.map { it.toString() }.orElse("empty")
341+
fun tokenInput(input: TokenInput): String = input.token.toString()
342+
fun otherTokens(input: OtherTokensInput): String = "${input.bean} ${input.map} ${input.node}"
343+
fun tokenNode(input: ObjectNode): String = input.get("token").let { if (it is POJONode) it.pojo.toString() else "$it" }
293344

294345
fun map(input: Map<String, Any?>) = receive("map", input)
295346
fun mapList(inputs: List<Map<String, Any?>>) = receive("mapList", inputs)
@@ -336,6 +387,16 @@ class CustomScalarInputTest {
336387

337388
data class Box(val content: Any)
338389

390+
class Token(private val value: String = "default") {
391+
override fun toString() = value
392+
}
393+
394+
data class TokenInput(val token: Token? = null)
395+
396+
data class OtherToken(val value: String = "other")
397+
398+
data class OtherTokensInput(val bean: OtherToken? = null, val map: Map<String, String>? = null, val node: ObjectNode? = null)
399+
339400
class BoxContentDeserializer : StdDeserializer<String>(String::class.java) {
340401
override fun deserialize(p: JsonParser, context: DeserializationContext): String =
341402
p.readValueAsTree<JsonNode>().path("content").stringValue("<missing>")
@@ -366,6 +427,7 @@ class CustomScalarInputTest {
366427
private val secretScalar = scalar("Secret") { Secret(it) }
367428
private val boxScalar = scalar("Box") { Box(if (it.startsWith("secret:")) Secret(it.removePrefix("secret:")) else it) }
368429
private val moneyScalar = scalar("Money") { Money(it.toInt(), "CAD") }
430+
private val tokenScalar = scalar("Token") { Token(it) }
369431

370432
private fun <T : Any> scalar(name: String, parse: (String) -> T): GraphQLScalarType = GraphQLScalarType.newScalar()
371433
.name(name)

0 commit comments

Comments
 (0)