Repository: mirumee/ariadne

Issue:
Bind Python Types to GraphQL Input Types [How To]
Perhaps it's just unclear in the docs, but I don't see any way to automatically convert from the input dictionaries given by Ariadne, to my resolvers, into custom Python types (i.e. dataclasses)

Is this supported? I found the docs on bindables, but from just that I can't tell if that's what I want. 

Example

```python
@dataclass
class Person:
    id: int
    name: str
    phone_number: str


@mutation.field('updatePerson')
def update_person(_, info, person: Person):
    # Currently what comes in is a dict. 
    # Is there a way to automatically map this to Person?
    do_update(
        person.id, 
        name=person.name, 
        phone=person.phone_number
    )
```


Discussion:
Hello!

Thank you for asking, but issue tracker is used exclusively for tracking bugs and roadmap. We are using [Spectrum](https://spectrum.chat/ariadne?tab=posts) for general Q&A and usage discussion.

A very similar question is already being discussed here: https://spectrum.chat/ariadne/general/how-to-access-resolver-arguments-as-custom-pydantic-any-other-objects-except-pure-dictionaries~8209983f-6e2d-4215-9976-d13d1e1acad2
@rafalp - the link you provided returns a 404. Do you know whether past discussions have been archived somewhere?
Yeah, Spectrum disappeared few months after GH bought them out.

What author asks about is not possible to do in "pure" GraphQL, and this is what vanilla Ariadne does. But it's something I'll keep on radar for [Ariadne GraphQL Modules](https://github.com/mirumee/ariadne-graphql-modules/issues/12).

In the meantime you would have to cast them manually in resolver or write custom decorator. Maybe we could have this decorator as an utility in Ariadne?
I'll reopen this because its something we could do in Ariadne proper, but not a high priority.
Thanks for the quick response @rafalp. I can see the benefit of such a utility being available in Ariadne, although to your point it's not unreasonable to write it in user-land.

In general, I think any ergonomic improvements to the relationship between the GraphQL schema and python type annotations is really powerful. While I haven't used it yet, the new `ariadne-codegen` package takes care of going from GraphQL schema -> pydantic models. So, adding some magic to the resolvers to make use of those models seems like a great next step.

While manually adding a decorator to a resolver is simple and safe, I'd like to briefly advocate for a "bolder" approach (more magic, but also more convenient): I think if Ariadne did some analysis of the resolver type signature at schema creation time (e.g. using [inspect.get_annotations](https://docs.python.org/3/library/inspect.html#inspect.get_annotations)) and added this casting utility decorator automatically for any resolvers whose annotations are `BaseModel` or `dataclass`, that would be pretty delightful.

As an aside: Given the really serious performance implications of using the built-in `middleware`, I have followed a similar pattern for "registering" utility decorators to run on subsets of the resolvers. Again, it's always possible to add the decorator manually, but for decorators that add logging context or trace information, it gets pretty unwieldy adding multiple decorators to almost every resolver.
Yes. Basically there's no way in GraphQL to tell query executor to represent value of argument or input field as an object other than scalar. And scalars only exist in GraphQL schemas as `scalar Name`, so there's no way to tell from schema what fields scalar has.

GraphQL lets us say what names should inputs and arguments values be passed to in Python, but that's it. So this conversion has to be done in the resolver, so best we could do would be to wrap user's resolver in custom resolver that does this conversion before passing the value to resolver proper.

So we would have to implement resolver decorator anyway, we would just need to have a "do_magic_args_types_conversion" option on `make_executable_schema` like how we already do it for cases conversion in Ariadne 0.18.
I was wrong! Apparently Inputs in graphql-core now have an `out_type` option which is an callable called with dict with input's values and is supposed to return Python representation of input. So we can put this on the roadmap :D
Ok, so `out_type` expects factory function that gets called with single argument (a `dict` with input data) and returns Python repr from this data.

Something like this doesn't work:

```
InputType("MyInput", MyDataclass)
```

But with small change it does:

```
InputType("MyInput", lambda data: MyDataclass(**data))
```

This opens interesting possibilities like using different Python type depending on input's contents themselves.

But I need to decide if Ariadne should funnel people to custom factory functions/lambda, or do it for them (which I am currently 90% against).

Resolving patch:
diff --git a/ariadne/__init__.py b/ariadne/__init__.py
--- a/ariadne/__init__.py
+++ b/ariadne/__init__.py
@@ -13,6 +13,7 @@
     get_formatted_error_traceback,
 )
 from .graphql import graphql, graphql_sync, subscribe
+from .inputs import InputType
 from .interfaces import InterfaceType, type_implements_interface
 from .load_schema import load_schema_from_path
 from .objects import MutationType, ObjectType, QueryType
@@ -43,6 +44,7 @@
     "ExtensionManager",
     "ExtensionSync",
     "FallbackResolversSetter",
+    "InputType",
     "InterfaceType",
     "MutationType",
     "ObjectType",
diff --git a/ariadne/executable_schema.py b/ariadne/executable_schema.py
--- a/ariadne/executable_schema.py
+++ b/ariadne/executable_schema.py
@@ -18,7 +18,9 @@
 from .types import SchemaBindable
 
 SchemaBindables = Union[
-    SchemaBindable, Type[Enum], List[Union[SchemaBindable, Type[Enum]]]
+    SchemaBindable,
+    Type[Enum],
+    List[Union[SchemaBindable, Type[Enum]]],
 ]
 
 
@@ -365,7 +367,9 @@ def join_type_defs(type_defs: List[str]) -> str:
     return "\n\n".join(t.strip() for t in type_defs)
 
 
-def normalize_bindables(*bindables: SchemaBindables) -> List[SchemaBindable]:
+def normalize_bindables(
+    *bindables: SchemaBindables,
+) -> List[SchemaBindable]:
     normal_bindables: List[SchemaBindable] = []
     for bindable in flatten_bindables(*bindables):
         if isinstance(bindable, SchemaBindable):
diff --git a/ariadne/inputs.py b/ariadne/inputs.py
new file mode 100644
--- /dev/null
+++ b/ariadne/inputs.py
@@ -0,0 +1,204 @@
+from typing import Dict, Optional, cast
+
+from graphql import GraphQLInputObjectType, GraphQLSchema
+from graphql.type.definition import GraphQLInputFieldOutType, GraphQLNamedType
+
+from .types import SchemaBindable
+
+
+class InputType(SchemaBindable):
+    """Bindable populating input types in a GraphQL schema with Python logic.
+
+    # Example input value represented as dataclass
+
+    Following code creates a GraphQL schema with object type named `Query`
+    with single field which has an argument of an input type. It then uses
+    the `InputType` to set `ExampleInput` dataclass as Python representation
+    of this GraphQL type:
+
+    ```python
+    from dataclasses import dataclass
+
+    from ariadne import InputType, QueryType, make_executable_schema
+
+    @dataclass
+    class ExampleInput:
+        id: str
+        message: str
+
+    query_type = QueryType()
+
+    @query_type.field("repr")
+    def resolve_repr(*_, input: ExampleInput):
+        return repr(input)
+
+    schema = make_executable_schema(
+        \"\"\"
+        type Query {
+            repr(input: ExampleInput): String!
+        }
+
+        input ExampleInput {
+            id: ID!
+            message: String!
+        }
+        \"\"\",
+        query_type,
+        # Lambda is used because out type (second argument of InputType)
+        # is called with single dict and dataclass requires each value as
+        # separate argument.
+        InputType("ExampleInput", lambda data: ExampleInput(**data)),
+    )
+    ```
+
+    # Example input with its fields mapped to custom dict keys
+
+    Following code creates a GraphQL schema with object type named `Query`
+    with single field which has an argument of an input type. It then uses
+    the `InputType` to set custom "out names" values, mapping GraphQL
+    `shortMessage` to `message` key in Python dict:
+
+    ```python
+    from ariadne import InputType, QueryType, make_executable_schema
+
+    query_type = QueryType()
+
+    @query_type.field("repr")
+    def resolve_repr(*_, input: dict):
+        # Dict will have `id` and `message` keys
+        input_id = input["id"]
+        input_message = input["message"]
+        return f"id: {input_id}, message: {input_message}"
+
+    schema = make_executable_schema(
+        \"\"\"
+        type Query {
+            repr(input: ExampleInput): String!
+        }
+
+        input ExampleInput {
+            id: ID!
+            shortMessage: String!
+        }
+        \"\"\",
+        query_type,
+        InputType("ExampleInput", out_names={"shortMessage": "message"}),
+    )
+    ```
+
+    # Example input value as dataclass with custom named fields
+
+    Following code creates a GraphQL schema with object type named `Query`
+    with single field which has an argument of an input type. It then uses
+    the `InputType` to set `ExampleInput` dataclass as Python representation
+    of this GraphQL type, and maps `shortMessage` input field to it's
+    `message` attribute:
+
+    ```python
+    from dataclasses import dataclass
+
+    from ariadne import InputType, QueryType, make_executable_schema
+
+    @dataclass
+    class ExampleInput:
+        id: str
+        message: str
+
+    query_type = QueryType()
+
+    @query_type.field("repr")
+    def resolve_repr(*_, input: ExampleInput):
+        return repr(input)
+
+    schema = make_executable_schema(
+        \"\"\"
+        type Query {
+            repr(input: ExampleInput): String!
+        }
+
+        input ExampleInput {
+            id: ID!
+            shortMessage: String!
+        }
+        \"\"\",
+        query_type,
+        InputType(
+            "ExampleInput",
+            lambda data: ExampleInput(**data),
+            {"shortMessage": "message"},
+        ),
+    )
+    ```
+    """
+
+    _out_type: Optional[GraphQLInputFieldOutType]
+    _out_names: Optional[Dict[str, str]]
+
+    def __init__(
+        self,
+        name: str,
+        out_type: Optional[GraphQLInputFieldOutType] = None,
+        out_names: Optional[Dict[str, str]] = None,
+    ) -> None:
+        """Initializes the `InputType` with a `name` and optionally out type
+        and out names.
+
+        # Required arguments
+
+        `name`: a `str` with the name of GraphQL object type in GraphQL schema to
+        bind to.
+
+        # Optional arguments
+
+        `out_type`: a `GraphQLInputFieldOutType`, Python callable accepting single
+        argument, a dict with data from GraphQL query, required to return
+        a Python representation of input type.
+
+        `out_names`: a `Dict[str, str]` with mappings from GraphQL field names
+        to dict keys in a Python dictionary used to contain a data passed as
+        input.
+        """
+        self.name = name
+        self._out_type = out_type
+        self._out_names = out_names
+
+    def bind_to_schema(self, schema: GraphQLSchema) -> None:
+        """Binds this `InputType` instance to the instance of GraphQL schema.
+
+        if it has an out type function, it assigns it to GraphQL type's
+        `out_type` attribute. If type already has other function set on
+        it's `out_type` attribute, this type is replaced with new one.
+
+        If it has any out names set, it assigns those to GraphQL type's
+        fields `out_name` attributes. If field already has other out name set on
+        its `out_name` attribute, this name is replaced with the new one.
+        """
+        graphql_type = schema.type_map.get(self.name)
+        self.validate_graphql_type(graphql_type)
+        graphql_type = cast(GraphQLInputObjectType, graphql_type)
+
+        if self._out_type:
+            graphql_type.out_type = self._out_type  # type: ignore
+
+        if self._out_names:
+            for graphql_name, python_name in self._out_names.items():
+                if graphql_name not in graphql_type.fields:
+                    raise ValueError(
+                        f"Field {graphql_name} is not defined on type {self.name}"
+                    )
+                graphql_type.fields[graphql_name].out_name = python_name
+
+    def validate_graphql_type(self, graphql_type: Optional[GraphQLNamedType]) -> None:
+        """Validates that schema's GraphQL type associated with this `InputType`
+        is an `input`."""
+        if not graphql_type:
+            raise ValueError("Type %s is not defined in the schema" % self.name)
+        if not isinstance(graphql_type, GraphQLInputObjectType):
+            raise ValueError(
+                "%s is defined in the schema, but it is instance of %s (expected %s)"
+                % (
+                    self.name,
+                    type(graphql_type).__name__,
+                    GraphQLInputObjectType.__name__,
+                )
+            )


Test patch:
diff --git a/tests/test_inputs.py b/tests/test_inputs.py
new file mode 100644
--- /dev/null
+++ b/tests/test_inputs.py
@@ -0,0 +1,167 @@
+from dataclasses import dataclass
+
+import pytest
+from graphql import graphql_sync
+
+from ariadne import InputType, make_executable_schema
+
+
+@pytest.fixture
+def schema():
+    return make_executable_schema(
+        """
+            type Query {
+                repr(input: ExampleInput!): Boolean!
+            }
+
+            input ExampleInput {
+                id: ID
+                message: String
+                yearOfBirth: Int
+            }
+        """
+    )
+
+
+def set_repr_resolver(schema, repr_resolver):
+    # pylint: disable=redefined-builtin
+    def resolve_repr(*_, input):
+        repr_resolver(input)
+        return True
+
+    schema.type_map["Query"].fields["repr"].resolve = resolve_repr
+
+
+TEST_QUERY = """
+query InputTest($input: ExampleInput!) {
+    repr(input: $input)
+}
+"""
+
+
+def test_attempt_bind_input_type_to_undefined_type_raises_error(schema):
+    input_type = InputType("Test")
+    with pytest.raises(ValueError):
+        input_type.bind_to_schema(schema)
+
+
+def test_attempt_bind_input_type_to_invalid_type_raises_error(schema):
+    input_type = InputType("Query")
+    with pytest.raises(ValueError):
+        input_type.bind_to_schema(schema)
+
+
+def test_attempt_bind_input_type_out_name_to_undefined_field_raises_error(schema):
+    input_type = InputType("Query", out_names={"undefined": "Ok"})
+    with pytest.raises(ValueError):
+        input_type.bind_to_schema(schema)
+
+
+def test_bind_input_type_out_type_sets_custom_python_type_for_input(schema):
+    # pylint: disable=redefined-builtin
+
+    @dataclass
+    class InputDataclass:
+        id: str
+        message: str
+        yearOfBirth: int
+
+    input_type = InputType(
+        "ExampleInput",
+        out_type=lambda data: InputDataclass(**data),
+    )
+    input_type.bind_to_schema(schema)
+
+    def assert_input_type(input):
+        assert isinstance(input, InputDataclass)
+        assert input.id == "123"
+        assert input.message == "Lorem ipsum"
+        assert input.yearOfBirth == 2022
+
+    set_repr_resolver(schema, assert_input_type)
+
+    result = graphql_sync(
+        schema,
+        TEST_QUERY,
+        variable_values={
+            "input": {
+                "id": "123",
+                "message": "Lorem ipsum",
+                "yearOfBirth": 2022,
+            }
+        },
+    )
+
+    assert not result.errors
+
+
+def test_bind_input_type_out_names_sets_custom_python_dict_keys_for_input(schema):
+    # pylint: disable=redefined-builtin
+
+    input_type = InputType(
+        "ExampleInput",
+        out_names={"yearOfBirth": "year_of_birth"},
+    )
+    input_type.bind_to_schema(schema)
+
+    def assert_input_type(input):
+        assert input == {
+            "id": "123",
+            "message": "Lorem ipsum",
+            "year_of_birth": 2022,
+        }
+
+    set_repr_resolver(schema, assert_input_type)
+
+    result = graphql_sync(
+        schema,
+        TEST_QUERY,
+        variable_values={
+            "input": {
+                "id": "123",
+                "message": "Lorem ipsum",
+                "yearOfBirth": 2022,
+            }
+        },
+    )
+
+    assert not result.errors
+
+
+def test_bind_input_type_out_type_and_names_sets_custom_python_type_for_input(schema):
+    # pylint: disable=redefined-builtin
+
+    @dataclass
+    class InputDataclass:
+        id: str
+        message: str
+        year_of_birth: int
+
+    input_type = InputType(
+        "ExampleInput",
+        out_type=lambda data: InputDataclass(**data),
+        out_names={"yearOfBirth": "year_of_birth"},
+    )
+    input_type.bind_to_schema(schema)
+
+    def assert_input_type(input):
+        assert isinstance(input, InputDataclass)
+        assert input.id == "123"
+        assert input.message == "Lorem ipsum"
+        assert input.year_of_birth == 2022
+
+    set_repr_resolver(schema, assert_input_type)
+
+    result = graphql_sync(
+        schema,
+        TEST_QUERY,
+        variable_values={
+            "input": {
+                "id": "123",
+                "message": "Lorem ipsum",
+                "yearOfBirth": 2022,
+            }
+        },
+    )
+
+    assert not result.errors
diff --git a/tests_mypy/inputs.py b/tests_mypy/inputs.py
new file mode 100644
--- /dev/null
+++ b/tests_mypy/inputs.py
@@ -0,0 +1,14 @@
+from dataclasses import dataclass
+
+from ariadne import InputType
+
+
+@dataclass
+class SomeInput:
+    id: str
+    message: str
+
+
+dataclass_input = InputType("Name", lambda data: SomeInput(**data))
+
+dict_input = InputType("Name", out_names={"fieldName": "field_name"})