https://kotlinlang.org logo
Title
j

james

06/22/2022, 2:02 AM
Any recommendations for handling a GraphQL query that returns a response field with type of
GraphQLJSONObject
from https://github.com/taion/graphql-type-json? Is it just a case of defining a custom type adapter here? If so, are they any known adapters baked already for turning this into a map of primitives?
m

mbonnin

06/22/2022, 7:15 AM
Hi 👋 Unfortunately we do not support mapping custom scalar to generic types (see https://github.com/apollographql/apollo-kotlin/issues/3243).
2 solutions: 1. you can wrap your map into a custom class:
class MyJsonObject(val fields: Map<String, Any?>)
register the mapping:
apollo {
  mapScalar("JSONObject", "com.example.MyJsonObject")
}
register the adapter:
apolloClientBuilder.addCustomScalarAdapter(JSONObject.type, object : Adapter<MyJsonObject> {
  override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): MyJsonObject {
    return MyJsonObject(reader.readAny() as Map<String, Any?>)
  }

  override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: MyJsonObject) {
    writer.writeAny(value.fields)
  }
})
2. you can use the builtin
AnyAdapter
:
apollo {
  mapScalarToKotlinAny("JSONObject")
}
2. is simpler but will requires casting to
Map<String, Any?>
when using such fields
s

Stylianos Gakis

06/22/2022, 9:01 AM
These are the available adapters right now if you’re curious. (Which note that I only found in the migration file in the docs. Maybe they deserve a spot in the normal docs as well for someone who isn’t migrating from 2.x. I couldn’t find them mentioned there, did I miss it?) We have the exact same use case and we’re doing the mapping to and from a JSONObject from a scalar we call JSONString like this. Then at least we get to work with JSONObject, which I personally don’t love but it seems to work for us. Here’s a test too.
m

mbonnin

06/22/2022, 9:36 AM
The doc for Apollo provided adapters is there. Moving forward, we'll certainly want to encourage using the Gradle options directly (
mapScalarToKotlinAny()
, etc...)
return JSONObject(reader.nextString()!!)
This only works if the Json is encoded as a Json string, right? (so there is some wrapping). I though the GraphQL
JSONObject
didn't require wrapping?
So for a schema like this:
scalar JSONObject
type Query {
  json: JSONObject
}
This is a valid response:
{
  "json": {
    "key": "value"
  }
}
But not this:
{
  "json": "{\"key\": \"value\"}"
}
s

Stylianos Gakis

06/22/2022, 9:44 AM
Thanks, I completely missed it! What do you mean by using the gradle options directly? To provide a custom adapter you’d still have to register the adapter in code, and sometimes the existing ones (like mapScalarToKotlinAny() etc) don’t suffice for our custom scalars. I probably misunderstood something.
This only works if the Json is encoded as a Json string, right?
Yes our JSONString scalar is used exactly like this, the backend sends a “proper” json string in there. It seems to work when there’s no indentation either here. What am I misunderstanding this time? 😄
m

mbonnin

06/22/2022, 9:47 AM
sometimes the existing ones (like mapScalarToKotlinAny() etc) don’t suffice for our custom scalars
It does now 🙂 . @bod added the possibility to pass the constructor/object adapter to
mapScalar
so that it is "baked" in the codegen:
fun mapScalar(graphQLName: String, targetName: String, expression: String)
Can be used like so:
mapScalar("Date", "com.example.Date", "com.example.DateAdapter")
where
DateAdapter
is an object implementing
Adapter<Date>
mapScalarToKotlinAny()
will "bake" the builtin
AnyAdapter
in the codegen. Obviously, you still need
apollo-adapters
in the classpath (we could add it automatically but we have decided not to touch the dependencies for
apollo-api
so it might be weird to do it for
apollo-adapters
)
Yes our JSONString scalar is used exactly like this, the backend sends a “proper” json string in there.
This is very fine but I think this is not what https://github.com/taion/graphql-type-json is about. I might be mistaken though. Because there's no real spec for this, different implementations do things differently
s

Stylianos Gakis

06/22/2022, 10:01 AM
One thing I was wondering about providing the path to the adapter in the gradle file to avoid submitting it in runtime was what happens when these adapters are in a different module. Currently we have one module which simply contains the .graphql[s] files and the apollo {} configuration in its gradle config. With that said however, I guess I could move the adapters in that module and it’d work. Hmm probably is a good idea.
m

mbonnin

06/22/2022, 10:02 AM
Yea, they need to be available when compiling the models
s

Stylianos Gakis

06/22/2022, 10:03 AM
Yeah maybe a bit weird to then have to add Adyen as a dependency in our apollo-only module in order to get the adapter baked in without having to provide it as a custom scalar adapter. Since we’re also serializing/deserializing an object coming from their SDK here. But at the same time, maybe this is fine, since we want apollo to deal with that type, it’s fine for the module to depend on that sdk just to bring in that type.
About that graphql-type-json spec, yeah I wasn’t even aware it exists, and it’s possible my backend team didn’t either and we are doing our own thing for this problem 😄
m

mbonnin

06/22/2022, 10:13 AM
On the plus side, you save a HashMap lookup for each custom scalar field by "baking" them. We have never really benchmarked this but that could be an argument
👌 2
j

james

06/23/2022, 10:13 AM
Thankyou both! Awesome discussion to wake up to. I’ve opted to go for your wrapped solution
class MyJsonObject(val fields: Map<String, Any?>)
for now and it works a treat. Glad I posted!
🎉 2