Skip to content

[Client] Can not config the custom scalar type for non jsonPrimitive. #1408

@timgreen

Description

@timgreen

Library Version

com.expediagroup..graphql-kotlin-client-generator 5.3.2

Describe the bug

assumes the raw type of the scalar to be converted is jsonPrimitive. Which is not true for custom scalar type JSONB in Hasura.

So it generates follow code

import ...omit....serialization.JsonScalarConverter
import com.expediagroup.graphql.client.Generated
import kotlin.Unit
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind.STRING
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive

@Generated
public object JsonObjectSerializer : KSerializer<JsonObject> {
  private val converter: JsonScalarConverter = JsonScalarConverter()

  public override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("JsonObject", STRING)

  public override fun serialize(encoder: Encoder, `value`: JsonObject): Unit {
    val encoded = converter.toJson(value)
    encoder.encodeString(encoded.toString())
  }

  public override fun deserialize(decoder: Decoder): JsonObject {
    val jsonDecoder = decoder as JsonDecoder
    val element = jsonDecoder.decodeJsonElement()
    val rawContent = element.jsonPrimitive.content
    return converter.toScalar(rawContent)
  }
}

Which causes runtime exception

Exception in thread "main" java.lang.IllegalArgumentException: Element class kotlinx.serialization.json.JsonObject is not a JsonPrimitive
        at kotlinx.serialization.json.JsonElementKt.error(JsonElement.kt:237)
        at kotlinx.serialization.json.JsonElementKt.getJsonPrimitive(JsonElement.kt:153)
        at ...omit....generated.scalars.JsonObjectSerializer.deserialize(JsonObjectSerializer.kt:30)
        at ...omit....generated.scalars.JsonObjectSerializer.deserialize(JsonObjectSerializer.kt:16)
        at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
        at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:35)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70)
        at ...omit....generated.getconfigsquery.configs$$serializer.deserialize(configs.kt:12)
        at ...omit....generated.getconfigsquery.configs$$serializer.deserialize(configs.kt:12)
        at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
        at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:35)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70)
        at kotlinx.serialization.encoding.CompositeDecoder$DefaultImpls.decodeSerializableElement$default(Decoding.kt:535)
        at kotlinx.serialization.internal.ListLikeSerializer.readElement(CollectionSerializers.kt:80)
        at kotlinx.serialization.internal.AbstractCollectionSerializer.readElement$default(CollectionSerializers.kt:51)
        at kotlinx.serialization.internal.AbstractCollectionSerializer.merge(CollectionSerializers.kt:36)
        at kotlinx.serialization.internal.AbstractCollectionSerializer.deserialize(CollectionSerializers.kt:43)
        at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
        at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:35)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70)
        at ...omit....generated.GetConfigsQuery$Result$$serializer.deserialize(GetConfigsQuery.kt:34)
        at ...omit....generated.GetConfigsQuery$Result$$serializer.deserialize(GetConfigsQuery.kt:34)
        at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
        at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:35)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)
        at kotlinx.serialization.encoding.AbstractDecoder.decodeNullableSerializableElement(AbstractDecoder.kt:79)
        at com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLResponse$$serializer.deserialize(KotlinxGraphQLResponse.kt:23)
        at com.expediagroup.graphql.client.serialization.types.KotlinxGraphQLResponse$$serializer.deserialize(KotlinxGraphQLResponse.kt:23)
        at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:59)
        at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:35)
        at kotlinx.serialization.json.Json.decodeFromString(Json.kt:100)
        at com.expediagroup.graphql.client.serialization.GraphQLClientKotlinxSerializer.deserialize(GraphQLClientKotlinxSerializer.kt:56)
        at com.expediagroup.graphql.client.serialization.GraphQLClientKotlinxSerializer.deserialize(GraphQLClientKotlinxSerializer.kt:34)
        at com.expediagroup.graphql.client.ktor.GraphQLKtorClient.execute$suspendImpl(GraphQLKtorClient.kt:47)
        at com.expediagroup.graphql.client.ktor.GraphQLKtorClient$execute$1.invokeSuspend(GraphQLKtorClient.kt)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147)
        at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15)
        at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147)
        at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15)
        at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147)
        at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15)
        at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at io.ktor.util.pipeline.SuspendFunctionGun.resumeRootWith(SuspendFunctionGun.kt:191)
        at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:147)
        at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15)
        at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base/java.lang.Thread.run(Thread.java:829)

To Reproduce
Steps to reproduce the behavior. Please provide:

  • Schema Configuration
  • Kotlin code used to generate the schema

build.gradle.kts

graphql {
    client {
        schemaFile = file("${project.projectDir}/src/main/resources/schema.graphql")
        packageName = "...omit..."
        serializer = GraphQLSerializer.KOTLINX
        customScalars = listOf(
            GraphQLScalar("jsonb", "kotlinx.serialization.json.JsonObject", "...omit....serialization.JsonScalarConverter"),
        )
    }
}

JsonScalarConverter.kt

class JsonScalarConverter : ScalarConverter<kotlinx.serialization.json.JsonObject> {
  override fun toScalar(rawValue: Any): kotlinx.serialization.json.JsonObject = Json.parseToJsonElement(rawValue.toString()).jsonObject
  override fun toJson(value: kotlinx.serialization.json.JsonObject): String = value.toString()
}

Expected behavior
Expected there is a way to use jsonb with graphql-kotlin client.

Suggested change

I'm not sure how common this use case are, but it is blocking me from using this library. If this could be supported, here is one backward compatible way to do it.

  1. update the generated code to pass the jsonElement instead of content
  public override fun deserialize(decoder: Decoder): JsonObject {
    val jsonDecoder = decoder as JsonDecoder
    val element = jsonDecoder.decodeJsonElement()
    return converter.elementToScalar(element)
  }
  1. add default implementation to the interface ScalarConverter. So the existing client usage can keep working.
interface ScalarConverter<T> {
    fun elementToScalar(rawValue: Any): T = toScalar(element.jsonPrimitive.content)

    ...
}

I'm happy to prepare PR if the idea looks good.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions