Why do you have to (re)declare types in GraphQL queries when you use variables?

I had a more general GraphQL question wrt to writing queries with variables…. You spend a lot of up-front time defining/honing your schema (input-objects, args, fields, etc) and you have this pretty thorough and self-describing interface as your contract with the rest of the world. Neat.

But when you write queries that use variables, suddenly, you have to re-declare your variable types… there’s nothing preventing you from mistyping your types or forgetting an !, and you end up having to crawl back through your definitions and telling GraphQL stuff that it seems like it should already know, e.g.

mutation ($x:String, $y:Stonks,$z:DejaVu,$zz:ForgotExclamationPoint){
    doThing(x:$x,y:$y,z:$z,zz:$zz) {
      foo
      bar
      etc
    }
}

This isn’t an Absinthe-specific question, but it doesn’t seem appropriate for StackOverflow either, so I’m hoping someone can shed light on why GraphQL is this way.

1 Like

AFAIK You should have auto completion with GraphiQL

https://www.apollographql.com/blog/graphql/basics/designing-graphql-mutations/#designing-the-mutation-input

Mutations should only ever have one input argument. That argument should be named input and should have a non-null unique input object type. In other words, your mutations should look like:

updatePost(input: { id: 4, newText: "..." }) { ... }
Instead of:
updatePost(id: 4, newText: "...") { ... }

But why? The reason is that the first style is much easier to use client-side. The client is only required to send one variable with per mutation instead of one for every argument on the mutation.

This seems like a small difference, but when you have mutations that need 10+ arguments your mutation GraphQL file will become much smaller.

mutation MyMutation($input: UpdatePostInput!) {
 updatePost(input: $input) { ... }
}

vs.

mutation MyMutation($id: ID!, $newText: String, ...) {
 updatePost(id: $id, newText: $newText, ...) { ... }
}
2 Likes

Thank you for the link, and yes, I think Graphiql does do the autocomplete. However,I’m still unclear on why the type must be effectively declared twice: once in the schema (the source of truth) and then redundantly in the query (or mutation). The purpose is unclear to me – it just seems like an unnecessary re-declaration that can only trip you up and serves no obvious functional benefit (?)

1 Like

Does this approach not avoid the duplication or are you referring to something else?

I think he’s referring to the idea that the type of the $input variable is known to the server without the client supplying UpdatePostInput!. That is to say, you could just do:

mutation MyMutation($input) {
 updatePost(input: $input) { ... }
}

or something. The server knows what type is expected by the updatePost mutation, so the client also typing out the type seems redundant.

@fireproofsocks I can’t speak to the designers goals. From experience however, having the variables and their associated types defined at the top helps in two ways:

  1. It allows client side type refinement and defaults
  2. It makes the “contract” of running the whole document explicit and obvious.

What I mean by (1) is this: If the server side schema allows an input to be null, the client can actually suffix the type declaration with ! to require that the input value is non null. This can be a useful sanity check if the client knows how they want to be using the document. Clients can also specify defaults eg: query ($state: String = "active") {.

The above is exactly the type of duplication that is bothering me: as @benwilson512 said, the server already knows type of the $input variable, so having the client re-define it in its request feels totally backwards in a “wag the dog” sort of way.

I struggle to think of a parallel with any other type of API… imagine if you coded server-side validation for a form submission, for example, and you meticulously defined the data types allowed for each field, but then you opened it up to the client (!!!) to dictate the values and types of the fields. The way the GraphQL syntax reads to me is something like having the client say “well, I know you wanted this field as an integer, but … nah, I’m gonna send you … I dunno… how about a nullable string?”

I know that you can’t really have the client override the types/validation because ultimately invalid requests will be rejected, but that just further demonstrates my point: the server has already defined the allowable data types for each input. It’s like subletting your apartment or something… you can do it, but you don’t have any real authority (and it goes against the lease).

Thank you @benwilson512 for providing a couple examples there of how the syntax could be used. I have to think about those use cases for a bit, but it seems more like a secondary “contract” is being created: this time between the user and the client instead of between the official contract between the client and server. I guess it would make a lot more sense to me if any “secondary” contracts (like forcing a value to be not-null when the server-side API allows nulls) were to be enforced elsewhere in the client’s code instead of in the GraphQL notation where it could be misleading.

1 Like

Defining just the one input type isn’t a big deal and hasn’t been any kind of issue for developers in company I work for.

But you can read the reason from the graphql-spec repo why it was done this way.

leebyron (Director of @graphql https://github.com/graphql Foundation.)

I think this is worth considering, variable definition is definitely one of the top stumbling points when learning GraphQL.

But it’s important to recognise that simply making the variable definitions optional does not simplify things, but instead shifts the complexity into other areas of the GraphQL system. We’ve strived for a net-most-simple system while designing GraphQL.

For example, defining your variables up front simplifies the validation logic both at static time by knowing if your definition of variables and usage of variables align, as well as reduces execution time by coercing your variable values once at the beginning of query execution rather than requiring a separate full query evaluation to extract variable types.

Another case where the defined variables is really useful is in client-side code generation. For example, graphql query text can be translated into a type-safe query runner that ensures you’ve provided all the right variables before sending the query over the network. We’ve built this at Facebook for our native apps, a non type safe variation of this is part of Relay, and I know Apollo’s iOS and Android clients are considering the same. Having all variables defined up front makes this code generation a 1:1. Needing to go collect the variables first requires walking the whole query, which for more complex apps could add considerable build time cost.

I think a potential solution to those issues is to just require defining variables when using tools or servers that rely on them, but that also could introduce support skew, which can be problematic for building an ecosystem of tools.

I see the value in making GraphQL easier to use in these cases though, so I certainly wouldn’t rule this out, but anyone who would like to champion this proposal and draft an RFC should consider its effect on the GraphQL validator and executor and be aware of implications for tool and server authors.

Thank you, that is a very helpful link that describes the issue and discussion surrounding it. Also interesting was the link to https://github.com/graphql/graphql-spec/issues/204#issuecomment-241879256

To summarize in my own words, the authors of GraphQL were thinking about it differently than I have been, in particular, the stated goal that “Fragments should be able to be validated in isolation.” “Isolation” here incurs a tradeoff that I had not even considered (due to my own lack of imagination I suppose): when things are isolated, there is no single source of truth. I.e. when you construct and validate a query in isolation, then YOU define the data types, NOT the API sitting on a remote server. The benefit of having the “freedom” of isolation is pitted against the sometimes sticky problems of how these queries or fragments shake out when you send them to the server. I don’t know enough about the benefits of this isolation, but it’s enlightening that it has been a subject of debate. For me, it would have been helpful if this “fragment isolation” feature had been discussed and repeated ad nauseam in the docs and examples because I have stumbled over this syntax for years.

I might be in the minority here in feeling that this syntax is fundamentally wrong, but now at least I understand more about why it is that way, and I now know that I’m not totally alone in finding it odd.

1 Like

Imagine you have a user schema.

The user schema has these fields:

object :user do
  field :id, :string

  field : name, :string

  field :organization_memberships, list_of: :membership do
    arg(:filter, :membership_filter)
    resolve(&my_resolver_function(&1, &2))
  end
end

That’s psuedo-code. The point is that the user schema has a field called organization_memberships that accepts a filter argument.

If I was querying for just, say, my own name, I would send this

query {
  me {
    name
    id
  }
}

but if I want only my “active” organization memberships, I would do this:

query($filter: OrganizationMembershipFilter) {
  me {
    id
    name
    organizationMemberships(filter: $filter) {
     status
     id
    }
  }
}

with variables, e.g.

{filter: {status: 'active'}}

or something like that.

So you can see that the number of arguments on the first line is never certain. It is conceivable that we may even use the same kind of filter twice.

For example, “show me all the archived organizations that my fellow active organization members are members of”:

query($myFilter: OrganizationMembershipFilter, $theirFilter: OrganizationMembershipFilter) {
  me {
    organizationMemberships(filter: $yFilter) {
      organization {
        memberships {
          user {
            organizationMemberships(filter: $theirFilter) {
              name
              status
           }
        }
      }
    }
  }
}

with variables

{myFilter: {status: 'active'}, theirFilter: {status: 'archived'}}

so you can see how number and type of arguments is, in a complex schema, fairly arbitrary.

2 Likes

Very good explanation, thank you for it.

2 Likes