How to resolve interfaces and union types in AppSync

Abstract types require a __typename field

Author's image
Tamás Sallai
3 mins

Abstract types in GraphQL

GraphQL supports abstract types where a field references a type that can be one of multiple concrete types when the resolver returns. They come in two varieties: interfaces and union types.

For example, let's say a schema defines 2 types of users, both implementing the same interface:

type AdminUser implements User {
	id: ID!
	permissions: [String!]!
}

type NormalUser implements User {
	id: ID!
}

interface User {
	id: ID!
}

Then a field can return the interface:

type Query {
	allUsers: [User!]!
}

Here, the query returns a list of users. Then the caller can define what fields to resolve for each concrete type using inline fragments:

query MyQuery {
	allUsers {
		id
		__typename
		... on AdminUser {
			id
			permissions
		}
		... on NormalUser {
			id
		}
	}
}

The result of this query depends on the type of the user objects, as the AdminUser has an extra permissions field.

Implementing resolvers for abstract types

This is all great on the schema level, but how does AppSync know if a user is AdminUser or NormalUser? Let's see the resolver implementation!

Let's say the users are stored in a database:

Then the resolver issues a Scan to fetch them:

{
	"version" : "2018-05-29",
	"operation" : "Scan"
}

Then the response mapping template returns the list:

#if($ctx.error)
	$util.error($ctx.error.message, $ctx.error.type)
#end
$utils.toJson($ctx.result.items)

Sending the query produces a rather strange error:

{
	"errors" : [
		{
			"errorType" : "BadRequestException",
			"message" : "Could not determine the exact type of User. Missing __typename key on value.'"
		}
	]
}

This message comes with a 400 status code, which is very rare in the GraphQL world. When a field produces an error it is usually a partial response with a 2XX status code. But in this particular case, this blows up the whole query.

Setting the __typename

So, what's the problem here?

AppSync can not know the type of the users, so it could not resolve the inline fragment. The solution is simple from here: convert the types from the database to a format AppSync understands. And this is, as stated in the error message, is the __typename:

#foreach($item in $ctx.result.items)
	#if($item.type == "admin")
		$util.qr($item.put("__typename", "AdminUser"))
	#else
		$util.qr($item.put("__typename", "NormalUser"))
	#end
#end
$utils.toJson($ctx.result.items)

The foreach iterates over the result items then checks the type field of each item. Then it adds a field to the item using the $util.qr($item.put(...)) structure. With this, the query runs as expected.

Why not add the __typename to the database directly? That would work also, but then it mixes two things: the GraphQL schema and the data model. Usually, a database shouldn't adapt to the API but the other way around, so it's better to add this check to the resolver rather than to the database.

The above example returned a list, so the resolver needed to use a foreach to iterate over the elements and add the field. But the same problem applies to single results too. In that case, there is no need for a foreach, just the .put(...).

And finally, notice that this needed per field that returns an abstract type. This leads to some code duplication as the __typename handling needs to be added to all fields' resolvers.

Union types

The same mechanism applies to union types too. For example, a search query can return documents and files:

type Document {
	id: ID!
	contents: String!
}

type File {
	id: ID!
	filename: String!
	url: String!
}

type Query {
	search(text: String!): [SearchResult!]!
}

union SearchResult = File | Document

Here, the results need to have a __typename that is either File or Document. Which means the resolver nees to add either:

$util.qr($item.put("__typename", "File"))

or:

$util.qr($item.put("__typename", "Document"))

When the __typename field is present for all items, the query runs just fine.

November 8, 2022

Free PDF guide

Sign up to our newsletter and download the "Foreign key constraints in DynamoDB" guide.


In this article