GraphQL in Kotlin : how to register generic scalars without sweating

If you are working like I do, you certainly are wrapping the smallest pieces of data to ensure they follow strict validation rules.
And you are likely to wrap the small field values in wrapping type, for these reasons :

– your IDE will tell you that you are manipulating an EmailAddress, an IPAddressV6 or a DomainName instead of a poor a char string.

– your tests will not need to concentrate on the possible data flows when irrelevant data is passed to downstream interactions

– your doc will be fluent to read, because you call a spade a spade.

– your code will stay human readable, because you will be able to add (a lot) of operations around your wrapping types (DomainName.getExtension(), EmailAddress.getDomainName(), IPAddressV6.isItPrivate())

These wrapper types can be bound to scalar in GraphQL, and I use graphql-kotlin to NOT need to create a clone of my mapping in a graphqls file (https://github.com/ExpediaGroup/graphql-kotlin)

But if you have plenty of them, you may end up wrapping each single wrapping type to a coercing function.

…Or even worse…

You will want to make all these independant wrapping types implement a type common interface.

Hang on, why don’t you make that generic and abstract, so you don’t have to care anymore about coercing in graphQL ?

Here is my attempt on the topic.

Whenever I use a wrapping type, I always use a @JsonCreator(mode = JsonCreator.Mode.DELEGATING) around it to make it also usable with downstream services not having graphQL but REST.

So during the schema creation, you may want to automatically create a coercing function for each like this below :

val alreadyCreatedWrapperTypes = mutableMapOf<KClass<*>, GraphQLScalarType>()
val hooks = object : SchemaGeneratorHooks {
   
override fun willGenerateGraphQLType(type: KType): GraphQLType? =
       
when (type.classifier as? KClass<*>) {
            
            in alreadyCreatedWrapperTypes.keys -> alreadyCreatedWrapperTypes[type.classifier]
           
else -> if ((type.classifier as? KClass<*>)?.constructors?.size == 1 &&
                (type.
classifier as? KClass<*>)?.constructors?.first()
                    ?.
annotations?.any { a ->
                   
a.
annotationClass == JsonCreator::class &&
                        (a
as JsonCreator).mode == JsonCreator.Mode.DELEGATING } == true) {
               
val newValue = wrappedValue<Any?>(type.classifier as KClass<*>)
                alreadyCreatedWrapperTypes[type.
classifier as KClass<*>] = newValue
                newValue
            }
else null
       
}
}

So above, you say that if I happen to find A @JsonCreator(DELEGATING) annotation on my data class and if I see only one constructor with only one parameter, then it certainly looks like a wrapper type.

Then after, you need to convert the wrapper type into the coercing function and to save the new scalar like this :

fun <T> wrappedValue(clazz: KClass<*>) = GraphQLScalarType.newScalar().name(
clazz.simpleName).description(
"Generic scalar (${clazz.simpleName})"
).coercing(object : Coercing<T, Any> {
override fun serialize(input: Any) =
(input::class.memberProperties.first() as KProperty<*>).getter.call(input)

override fun parseValue(input: Any): T =
clazz.constructors.first().call(input) as T

override fun parseLiteral(input: Any): T =
when (input) {
is StringValue -> clazz.constructors.first().call(input.value) as T
is IntValue -> clazz.constructors.first().call(input.value.toInt()) as T
is LongValue -> clazz.constructors.first().call(input.value) as T
else -> throw CoercingParseLiteralException("Invalid input '$input' for wrapped value")
}
}).build()

This will result in wrapped types that will keep their validation during the API call, but you don’t need to wrap them from the client.


e.g, use this syntax : {register(emailAddress: “john.doe@acme.com”){…}} and get the EmailAddress constructor validation for free.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.