GQLGEN Panic: Fixing Recursive Input Unmarshalling
Are you encountering a panic when using gqlgen with recursive input types like TodoWhereInput and mapping them to map[string]interface{}? You're not alone! This article dives deep into the issue, explains the root cause, provides a clear reproduction scenario, and offers a path towards a solution. Let's unravel this common gqlgen hurdle and get your GraphQL server back on track.
The Problem: Unmarshalling Recursive Input Slices in GQLGEN
When working with GraphQL APIs and the gqlgen library, developers often encounter challenges when dealing with complex input types, especially those that are recursive or contain nested lists. The core issue arises when a list field of an input object is mapped to map[string]interface{} within the gqlgen.yml configuration file. Specifically, the generated unmarshalling code, intended to handle the conversion of input data into Go-friendly structures, falls short when it encounters slices of these recursive input types. This results in a runtime panic, disrupting the expected behavior of the GraphQL server.
The heart of the problem lies in how gqlgen generates the unmarshalling functions. These functions are responsible for taking the incoming data, which is often in a JSON-like format, and converting it into the Go data structures that your resolvers expect. When a list field is encountered, the generated code incorrectly attempts to pass the entire slice of data (as []interface{}) directly to a single-item unmarshaller, instead of iterating over the slice and unmarshalling each individual element. This mismatch in expectations leads to an interface conversion error, specifically panic: interface conversion: interface {} is []interface {}, not map[string]interface {}, as the code attempts to treat a slice as a single map.
Understanding this issue requires a grasp of how gqlgen processes the GraphQL schema and generates the necessary Go code. When you define an input type with a recursive relationship (i.e., a field that references the input type itself, such as the AND field in the TodoWhereInput example), gqlgen must generate code that can handle this nested structure. The incorrect generation of the unmarshalling function for the list field, however, breaks this process, leading to the observed panic. The impact of this issue is significant, as it prevents the server from correctly parsing and processing queries that use these recursive or nested input types, thereby limiting the functionality of the API.
Detailed Breakdown
Let's break down the scenario with the TodoWhereInput example:
-
Schema Definition:
input TodoWhereInput { textContains: String done: Boolean AND: [TodoWhereInput!] } type Query { todos(where: TodoWhereInput): [Todo!]! }This GraphQL schema defines a structure where
TodoWhereInputcan contain a list of itself through theANDfield. This allows for complex filtering conditions. -
gqlgen.yml Configuration:
models: TodoWhereInput: model: - map[string]interface{}This configuration tells
gqlgento map theTodoWhereInputto amap[string]interface{}in Go. This is a common practice when you want to handle flexible or dynamic input structures. -
The Issue:
The generated code (in
graph/generated.go) contains an incorrect unmarshalling function. It tries to unmarshal the entire slice ofTodoWhereInputdirectly, rather than iterating over its elements. This causes a type mismatch and the subsequent panic.
Steps to Reproduce the Panic
To fully understand and replicate the issue, follow these detailed steps. This will help you verify the problem in your own gqlgen projects and understand how to identify the problematic code. Each step is crucial to recreating the error and seeing the panic occur.
-
Define a Recursive Input Type in Your GraphQL Schema: Start by creating an input type that references itself or contains a list of its own type. This recursive nature is the key to triggering the bug. A typical example would be the
TodoWhereInputdemonstrated earlier. Ensure your schema includes at least one query that utilizes this input type.input TodoWhereInput { textContains: String done: Boolean AND: [TodoWhereInput!] } type Query { todos(where: TodoWhereInput): [Todo!]! } -
Configure
gqlgen.ymlto Map the Input Type tomap[string]interface{}: In yourgqlgen.ymlfile, instructgqlgento map your recursive input type to amap[string]interface{}in your Go code. This mapping is where the unmarshalling problems will arise, especially with slices.models: TodoWhereInput: model: - map[string]interface{} -
Run Your GraphQL Server and Execute a Query with the List Field: Compile and run your GraphQL server. Then, execute a query that uses the list field of your recursive input type. This query should include a nested structure to fully test the unmarshalling of the list. Here's an example query using the
ANDfield:{ todos(where: {AND: [{textContains: