graphene icon indicating copy to clipboard operation
graphene copied to clipboard

Default value for custom input object type

Open tlinhart opened this issue 7 years ago • 14 comments

I have a custom input object type

class Filters(graphene.InputObjectType):                                        
    name = graphene.String()
    type = graphene.List(graphene.String)

and the query definition

class Query(graphene.ObjectType):                                               
    search = graphene.Field(
        SearchResult, query=graphene.String(required=True),
        filters=graphene.Argument(Filters, default_value=None)
    )

I want to be able to provide an empty default value for the filters argument. With the above definition, the argument is not set as optional and I'm getting this error message:

{
  "errors": [
    {
      "message": "resolve_search() missing 1 required positional argument: 'filters'",
      "locations": [
        {
          "line": 15,
          "column": 3
        }
      ],
      "path": [
        "search"
      ]
    }
  ],
  "data": {
    "search": null
  }
}

What is the correct way to provide an empty default value for the argument of custom object type? I'm at these package versions:

graphene==2.1.3 graphql-core==2.1 graphql-server-core==1.1.1

tlinhart avatar Nov 06 '18 08:11 tlinhart

I usually do it in the resolver:


class MyQuery(...):
    ...
    
    def resolver(root, info, filters=ABC):
        pass

does this help? :)

patrick91 avatar Nov 06 '18 09:11 patrick91

Not really, for two reasons. When I define default value in the resolver, it's not advertised in a schema, for example in GraphiQL I don't see the default value for filters argument. That's why I specify the default value in field definition. The second reason is that I specifically don't know how to define an empty value of the Filters type. If I try to use filters=Filters() I get this error:

{
  "errors": [
    {
      "message": "argument of type 'List' is not iterable",
      "locations": [
        {
          "line": 15,
          "column": 3
        }
      ],
      "path": [
        "search"
      ]
    }
  ],
  "data": {
    "search": null
  }
}

When I try to use empty dict as the default value, I get this error:

{
  "errors": [
    {
      "message": "'dict' object has no attribute 'project'",
      "locations": [
        {
          "line": 15,
          "column": 3
        }
      ],
      "path": [
        "search"
      ]
    }
  ],
  "data": {
    "search": null
  }
}

However, when I query the schema like this

query Search {
  search(query: "some text", filters: {}) {
    count
  }
}

I get the data without error.

tlinhart avatar Nov 06 '18 15:11 tlinhart

After a lot of experimenting, I've found a workaround. If I define class

class DefaultFilters(object):
    name = None
    type = None

I can then use its instance as a default value in field definition:

class Query(graphene.ObjectType):                                               
    search = graphene.Field(
        SearchResult, query=graphene.String(required=True),
        filters=Filters(default_value=DefaultFilters)
    )

Although this works, I don't believe this is the correct way to achive the goal.

tlinhart avatar Nov 06 '18 16:11 tlinhart

Yeah using Filters() should work, so I guess there's a bug somewhere :)

I'll see if I can have a look at it soon

patrick91 avatar Nov 06 '18 16:11 patrick91

class Query(graphene.ObjectType):
    search = graphene.Field(
        SearchResult,
        query=graphene.String(required=True),
        filters=graphene.Argument(Filters, default_value={})
    )

Seems to work and produces

type Query {
  search(query: String!, filters: Filters = {}): SearchResult
}

I also tried it with your DefaultFilters but I couldn't output the schema for some reason.

cherls avatar Nov 06 '18 20:11 cherls

@cherls Yeah, it produces the correct schema (or at least the schema I except and I'm happy with), but the problem is with execution. When I execute the query and don't provide filters argument to search field, I get this error:

{
  "errors": [
    {
      "message": "'dict' object has no attribute 'project'",
      "path": [
        "search"
      ],
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ],
  "data": {
    "search": null
  }
}

Thinking about it, I guess Graphene is just about schema creation and not concerned with execution, right?

tlinhart avatar Nov 07 '18 07:11 tlinhart

@tlinhart

Execution works fine for me with the following code. Note that the filters argument is missing on the search field. I think the error is something else in your code.

import json

import graphene


class SearchResult(graphene.ObjectType):
    result = graphene.String()


class Filters(graphene.InputObjectType):
    name = graphene.String()
    type = graphene.List(graphene.String)


class Query(graphene.ObjectType):
    search = graphene.Field(
        SearchResult,
        query=graphene.String(required=True),
        filters=graphene.Argument(Filters, default_value={})
    )

    def resolve_search(self, info, query, filters):
        return SearchResult(result="result for " + query)


schema = graphene.Schema(query=Query)

result = schema.execute(
    '''
    query {
        search (query: "test query") {
            result
        }
    }
    '''
)

print(json.dumps(result.data))

with open('schema.graphql', 'w') as fp:
    fp.write(str(schema))

cherls avatar Nov 07 '18 15:11 cherls

I don't execute the query directly, I use run_http_query and encode_execution_results functions from graphql-server-core package. But I'm pretty sure that's not the issue.

After some debugging, I think I found the source of the problem. Inside the resolver, I work with filters argument as of type Filters i.e. I use dotted notation to access the attributes (e.g. filters.type). But when I define the default value as {} and don't supply the value in a query, inside the resolver filters is of type dict and thus I receive the 'dict' object has no attribute 'type' error message during execution. When I provide a value in the query (e.g. search(query: "some query", filters: {type: TYPE1}) { result } where TYPE1 is an enum value), inside the resolver filters is of type Filters and dotted notation works. Also the schema can be dumped using fp.write(str(schema)).

If I define the default value as Filters() (i.e. empty instance), inside the resolver filters is correctly of type Filters. However, if I don't supply a value in a query and try to print the value in the resolver, I get <schema.Filters object at 0x7f6892f544e0> (serialization doesn't work). If I provide a value in the query (e.g. search(query: "some query", filters: {type: TYPE1}) { result }), filters is of type Filters inside the resolver and printing the value correctly outputs {'type': ['type1']}.

Another problem is that with Filters() as a default value, schema cannot be dumped. If I try fp.write(str(schema)), it produces this error:

Traceback (most recent call last):
  File "/home/tmp/test.py", line 59, in <module>
    fp.write(str(schema))
  File "/home/tmp/venv/lib/python3.4/site-packages/graphene/types/schema.py", line 111, in __str__
    return print_schema(self)
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 31, in print_schema
    schema, lambda n: not (is_spec_directive(n)), _is_defined_type
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 75, in _print_filtered_schema
    for typename, type in sorted(schema.get_type_map().items())
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 76, in <listcomp>
    if type_filter(typename)
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 108, in _print_type
    return _print_object(type)
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 138, in _print_object
    type.name, implemented_interfaces, _print_fields(type)
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 174, in _print_fields
    for f_name, f in type.fields.items()
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 174, in <genexpr>
    for f_name, f in type.fields.items()
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 198, in _print_args
    for arg_name, arg in field_or_directives.args.items()
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 198, in <genexpr>
    for arg_name, arg in field_or_directives.args.items()
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/schema_printer.py", line 206, in _print_input_value
    default_value = " = " + print_ast(ast_from_value(arg.default_value, arg.type))
  File "/home/tmp/venv/lib/python3.4/site-packages/graphql/utils/ast_from_value.py", line 55, in ast_from_value
    assert isinstance(value, dict)
AssertionError

I naively assumed that this kind of things is handled automatically by Graphene. Is there something that I'm missing?

tlinhart avatar Nov 08 '18 13:11 tlinhart

@cherls Using default_value={} I get the correct schema definition but the value passed eventually to my resolver is the empty dictionary but not my custom input type with default values I was expecting. Am I missing something or is that the intended behavior?

artemnesterenko avatar Dec 06 '18 07:12 artemnesterenko

For now, I've ended up with the following solution:

import graphene

class InputObjectType(graphene.InputObjectType):

    @classmethod
    def default(cls):
        meta = cls._meta
        fields = meta.fields
        default_fields = {name: field.default_value for name, field in fields.items()}
        container = meta.container
        return container(**default_fields)


class SizeInput(InputObjectType):
    width = graphene.Int(default_value=100)
    height = graphene.Int(default_value=100)

    def square(self):
        return self.width * self.height

Since now, it's possible to do:


class Query(graphene.ObjectType):
    square = graphene.Field(
        graphene.NonNull(graphene.Int),
        size=graphene.Argument(SizeInput, default_value=SizeInput.default())
    )
    
    @staticmethod
    def resolve_square(root, info, size, **kwargs):
        return size.square()

It produces the correct schema and passes the default value I was expecting instead of the empty dict.

artemnesterenko avatar Dec 06 '18 08:12 artemnesterenko

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Jul 29 '19 21:07 stale[bot]

class InputObjectType(graphene.InputObjectType):

    @classmethod
    def default(cls):
        meta = cls._meta
        fields = meta.fields
        default_fields = {name: field.default_value for name, field in fields.items()}
        container = meta.container
        return container(**default_fields)

Brilliant, this should be part of Graphene

rlancer avatar Jul 02 '20 14:07 rlancer

Agreed!

jkimbo avatar Jul 02 '20 16:07 jkimbo

Is @artemnesterenko 's comment still the best option to solve this issue in 2024?

lgnashold avatar Oct 16 '24 22:10 lgnashold