1

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!

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
jmandt
  • 386
  • 5
  • 13

1 Answers1

1

jkimbo answered the question here:

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

jmandt
  • 386
  • 5
  • 13