caliban icon indicating copy to clipboard operation
caliban copied to clipboard

Handling `extend type` directive

Open satorg opened this issue 7 months ago • 3 comments

GraphQL spec: 3.6.3 Object Extensions

Currently Caliban can parse it, but cannot generate. Sometimes it leads to issues when a GraphQL schema containing extend type is provided "as is" and cannot be changed, but has to be complied with.

Therefore it could come in really handy to have a possibility to generate extend type too. Note that a GraphQL schema can have multiple extend type directives for the same type.


Although the feature could be implemented in many different ways, here's one of the ways I could think of personally.

For example, consider the following schema:

type TheQuery {
  one: String
}
# first extension
extend type TheQuery {
  two: Int
  three: Float
}
# second extension
extend type TheQuery {
  four: Boolean
}

One of the way to handle it could be a new annotation, e.g. @GQLExtendType, which would allow to parse/generate the following model:

case class TheQuery(
  one: String,
  @GQLExtendType // opens "first extension"
  two: Int,
  three: Float,
  @GQLExtendType // opens "second extension"
  four: Boolean
)

In other words, @GQLExtendType is applied to a field that opens a new extension group. Each group corresponds to a separate extend type directive in GraphQL. The group continues until the end of the case class or until the next @GQLExtendType is encountered.

satorg avatar May 30 '25 17:05 satorg

I like the simplicity of the proposed approach, but I think it might box us in a bit on this bit:

Object type extensions may choose not to add additional fields, instead only adding interfaces or directives.

If we were to go this route of augmenting the existing schema I'd suggest doing it through a nested component

case class User(
  id: ID,
  name: String
  @GQLExtend extension1: UserExt
  @GQLExtend extension2: NodeExt
)

case class UserExt(
  age: Int,
)

@GQLDirective(Foo(42))
object NodeExt extends Node

The idea would be that each extend type is flattened into the main type for execution but we can preserve the extension state for rendering the schema, and can write this format when generating a schema.

There are some other variations of this approach that we could take too depending on how we the merging would work at a schema level.

We could do an inverted form of extension by wrapping the type.

@GQLExtendWith
case class Wrapper(
  main: Foo,
  ext1: UserExt,
  ext2: NodeExt

Where the convention would be for the first item to be the base type and all subsequent ones would be merged in.

Both these would be declarative and they allow some benefit beyond the stated addition of allowing schemas to be DRYer.

Alternatively,

We could do extension at the GraphQL level, we already have a merge operator, we could add a new extend method that treats the second api as adding extensions.

paulpdaniels avatar Jun 01 '25 06:06 paulpdaniels

case class User(
  id: ID,
  name: String
  @GQLExtend extension1: UserExt
  @GQLExtend extension2: NodeExt
)

I thought about the nesting approach too, but my concern here is – if we are to generate model classes from GraphQL schema (via caliban codegen), how would the code generator choose appropriate names for those nesting fields?

satorg avatar Jun 01 '25 06:06 satorg

We can just pick some heuristic, structurally it wouldn't make a difference since it wouldn't be part of the schema

paulpdaniels avatar Jul 06 '25 06:07 paulpdaniels