I am setting up a GraphQL Server with Python using Starlette and Graphene and ran into a problem I cannot find a solution for. The Graphene Documentation does not go into detail regarding the union type, which I am trying to implement. I set up a minimum example based on the graphene documentation which you can run to replicate this problem
import os
import uvicorn
from graphene import ObjectType, Field, List, String, Int, Union
from graphene import Schema
from starlette.applications import Starlette
from starlette.graphql import GraphQLApp
from starlette.routing import Route
mock_data = {
"episode": 3,
"characters": [
{
"type": "Droid",
"name": "R2-D2",
"primaryFunction": "Astromech"
},
{
"type": "Human",
"name": "Luke Skywalker",
"homePlanet": "Tatooine"
},
{
"type": "Starship",
"name": "Millennium Falcon",
"length": 35
}
]
}
class Human(ObjectType):
name = String()
homePlanet = String()
class Droid(ObjectType):
name = String()
primary_function = String()
class Starship(ObjectType):
name = String()
length = Int()
class Characters(Union):
class Meta:
types = (Human, Droid, Starship)
class SearchResult(ObjectType):
characters = List(Characters)
episode = Int()
class RootQuery(ObjectType):
result = Field(SearchResult)
@staticmethod
def resolve_result(_, info):
return mock_data
graphql_app = GraphQLApp(schema=Schema(query=RootQuery))
routes = [
Route("/graphql", graphql_app),
]
api = Starlette(routes=routes)
if __name__ == "__main__":
uvicorn.run(api, host="127.0.0.1", port=int(os.environ.get("PORT", 8080)))
If you then go to http://localhost:8080/graphq and enter the following query
query Humans{
result {
episode
characters {
... on Human {
name
}
}
}
}
I get this error
{
"data": {
"result": {
"episode": 3,
"characters": null
}
},
"errors": [
{
"message": "Abstract type Characters must resolve to an Object type at runtime for field SearchResult.characters with value \"[{'type': 'Droid', 'name': 'R2-D2', 'primaryFunction': 'Astromech'}, {'type': 'Human', 'name': 'Luke Skywalker', 'homePlanet': 'Tatooine'}, {'type': 'Starship', 'name': 'Millennium Falcon', 'length': 35}]\", received \"None\".",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
which I am now stuck with. Maybe someone has done this already and can help out? How can I resolve this at runtime. I have already tried different approaches for example I changed classes Character and RootQuery:
class Character(Union):
class Meta:
types = (Human, Droid, Starship)
def __init__(self, data, *args, **kwargs):
super().__init__(*args, **kwargs)
self.data = data
self.type = data.get("type")
def resolve_type(self, info):
if self.type == "Human":
return Human
if self.type == "Droid":
return Droid
if self.type == "Starship":
return Starship
class RootQuery(ObjectType):
result = Field(SearchResult)
@staticmethod
def resolve_result(_, info):
return {**mock_data, "characters": [Character(character) for character in mock_data.get('characters')]}
resulting in
{
"data": {
"result": {
"episode": 3,
"characters": [
{},
{
"name": null
},
{}
]
}
}
}
Any ideas would be very appreciated!
@jmandt your solution is the correct one but you need to update your query to select fields on the different types:
query Humans{
result {
episode
characters {
... on Droid {
name
}
... on Starship {
name
}
... on Human {
name
}
}
}
}
@jkimbo: Thanks for the quick answer. I have tried this before, but this returns the below result, in which all properties of characters are always null.
{
"data": {
"result": {
"episode": 3,
"characters": [
{
"name": null
},
{
"name": null
},
{
"name": null
}
]
}
}
}
@jmandt try this:
class Character(Union):
class Meta:
types = (Human, Droid, Starship)
@classmethod
def resolve_type(cls, instance, info):
if instance["type"] == "Human":
return Human
if instance["type"] == "Droid":
return Droid
if instance["type"] == "Starship":
return Starship
class RootQuery(ObjectType):
result = Field(SearchResult)
def resolve_result(_, info):
return mock_data
Note I'm just returning mock_data and I've updated the resolve_type method to switch based on the data. The Union type uses the same resolve_type method as Interface to figure out what type to resolve to at runtime: https://docs.graphene-python.org/en/latest/types/interfaces/#resolving-data-objects-to-types
@jkimbo this works. Thanks a lot.
@jkimbo this works. Thanks a lot.
How it worked? It returns only 1 class attributes because in resolve_type is return to the 1 class, and not all attributes from union-ed classes.
Thanks!
@R1nat-P it does not have anything to do with the union-ed class. instance is the actual data with which the class is called and instance["type"] is just the attribute. You could also do with instance.get("type"). I suggest you to have a closer look at the source code or even set the example up your self locally. Btw, here you can find the stackoverflow article corresponding to it. Hope this helps.
@R1nat-P it does not have anything to do with the union-ed class. instance is the actual data with which the class is called and instance["type"] is just the attribute. You could also do with instance.get("type"). I suggest you to have a closer look at the source code or even set the example up your self locally. Btw, here you can find the stackoverflow article corresponding to it. Hope this helps.
@jmandt ,
Actually was done like was described in official documentation link , therefore was added the part with resolve_type , no luck.
class LoginData(graphene.Union):
class Meta:
types = (UsersData, MembershipData)
@classmethod
def resolve_type(cls, instance, info):
if instance["type"] == "UsersData":
return UsersData
if instance["type"] == "MembershipData":
return MembershipData
And getting the following result
Thanks!
Most helpful comment
How it worked? It returns only 1 class attributes because in resolve_type is return to the 1 class, and not all attributes from union-ed classes.
Thanks!