Skip to content

Commit 37f1917

Browse files
authored
Custom Scalars input serialization in variables (#253)
1 parent d5e3e6d commit 37f1917

11 files changed

+1338
-12
lines changed

docs/modules/gql.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ Sub-Packages
2121
client
2222
transport
2323
dsl
24+
utilities

docs/modules/utilities.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
gql.utilities
2+
=============
3+
4+
.. currentmodule:: gql.utilities
5+
6+
.. automodule:: gql.utilities

docs/usage/custom_scalars.rst

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
Custom Scalars
2+
==============
3+
4+
Scalar types represent primitive values at the leaves of a query.
5+
6+
GraphQL provides a number of built-in scalars (Int, Float, String, Boolean and ID), but a GraphQL backend
7+
can add additional custom scalars to its schema to better express values in their data model.
8+
9+
For example, a schema can define the Datetime scalar to represent an ISO-8601 encoded date.
10+
11+
The schema will then only contain:
12+
13+
.. code-block:: python
14+
15+
scalar Datetime
16+
17+
When custom scalars are sent to the backend (as inputs) or from the backend (as outputs),
18+
their values need to be serialized to be composed
19+
of only built-in scalars, then at the destination the serialized values will be parsed again to
20+
be able to represent the scalar in its local internal representation.
21+
22+
Because this serialization/unserialization is dependent on the language used at both sides, it is not
23+
described in the schema and needs to be defined independently at both sides (client, backend).
24+
25+
A custom scalar value can have two different representations during its transport:
26+
27+
- as a serialized value (usually as json):
28+
29+
* in the results sent by the backend
30+
* in the variables sent by the client alongside the query
31+
32+
- as "literal" inside the query itself sent by the client
33+
34+
To define a custom scalar, you need 3 methods:
35+
36+
- a :code:`serialize` method used:
37+
38+
* by the backend to serialize a custom scalar output in the result
39+
* by the client to serialize a custom scalar input in the variables
40+
41+
- a :code:`parse_value` method used:
42+
43+
* by the backend to unserialize custom scalars inputs in the variables sent by the client
44+
* by the client to unserialize custom scalars outputs from the results
45+
46+
- a :code:`parse_literal` method used:
47+
48+
* by the backend to unserialize custom scalars inputs inside the query itself
49+
50+
To define a custom scalar object, we define a :code:`GraphQLScalarType` from graphql-core with
51+
its name and the implementation of the above methods.
52+
53+
Example for Datetime:
54+
55+
.. code-block:: python
56+
57+
from datetime import datetime
58+
from typing import Any, Dict, Optional
59+
60+
from graphql import GraphQLScalarType, ValueNode
61+
from graphql.utilities import value_from_ast_untyped
62+
63+
64+
def serialize_datetime(value: Any) -> str:
65+
return value.isoformat()
66+
67+
68+
def parse_datetime_value(value: Any) -> datetime:
69+
return datetime.fromisoformat(value)
70+
71+
72+
def parse_datetime_literal(
73+
value_node: ValueNode, variables: Optional[Dict[str, Any]] = None
74+
) -> datetime:
75+
ast_value = value_from_ast_untyped(value_node, variables)
76+
return parse_datetime_value(ast_value)
77+
78+
79+
DatetimeScalar = GraphQLScalarType(
80+
name="Datetime",
81+
serialize=serialize_datetime,
82+
parse_value=parse_datetime_value,
83+
parse_literal=parse_datetime_literal,
84+
)
85+
86+
Custom Scalars in inputs
87+
------------------------
88+
89+
To provide custom scalars in input with gql, you can:
90+
91+
- serialize the scalar yourself as "literal" in the query:
92+
93+
.. code-block:: python
94+
95+
query = gql(
96+
"""{
97+
shiftDays(time: "2021-11-12T11:58:13.461161", days: 5)
98+
}"""
99+
)
100+
101+
- serialize the scalar yourself in a variable:
102+
103+
.. code-block:: python
104+
105+
query = gql("query shift5days($time: Datetime) {shiftDays(time: $time, days: 5)}")
106+
107+
variable_values = {
108+
"time": "2021-11-12T11:58:13.461161",
109+
}
110+
111+
result = client.execute(query, variable_values=variable_values)
112+
113+
- add a custom scalar to the schema with :func:`update_schema_scalars <gql.utilities.update_schema_scalars>`
114+
and execute the query with :code:`serialize_variables=True`
115+
and gql will serialize the variable values from a Python object representation.
116+
117+
For this, you need to provide a schema or set :code:`fetch_schema_from_transport=True`
118+
in the client to request the schema from the backend.
119+
120+
.. code-block:: python
121+
122+
from gql.utilities import update_schema_scalars
123+
124+
async with Client(transport=transport, fetch_schema_from_transport=True) as session:
125+
126+
update_schema_scalars(session.client.schema, [DatetimeScalar])
127+
128+
query = gql("query shift5days($time: Datetime) {shiftDays(time: $time, days: 5)}")
129+
130+
variable_values = {"time": datetime.now()}
131+
132+
result = await session.execute(
133+
query, variable_values=variable_values, serialize_variables=True
134+
)

docs/usage/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ Usage
1010
variables
1111
headers
1212
file_upload
13+
custom_scalars

0 commit comments

Comments
 (0)