File: data_model_and_resolvers.md

package info (click to toggle)
python-apischema 0.18.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 1,608 kB
  • sloc: python: 15,266; sh: 7; makefile: 7
file content (178 lines) | stat: -rw-r--r-- 8,441 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# Data model and resolvers

Almost everything in the [Data model section](../data_model.md) remains valid in GraphQL integration, with a few differences.

## GraphQL specific data model

### `Enum`

`Enum` members are represented in the schema using their **name** instead of their value. This is more consistent with the way GraphQL represents enumerations.

### `TypedDict`

`TypedDict` is not supported as an output type. (see [FAQ](#why-typeddict-is-not-supported-as-an-output-type))

### `Union`
Unions are only supported between **output** object type, which means `dataclass` and `NamedTuple` (and [conversions](../conversions.md)/[dataclass model](../conversions.md#dataclass-model---automatic-conversion-fromto-dataclass)).

There are 2 exceptions which can be always be used in `Union`:

- `None`/`Optional`: Types are non-null (marked with an exclamation mark `!` in GraphQL schema) by default; `Optional` types however results in normal GraphQL types (without `!`).
- `apischema.UndefinedType`: it is simply ignored. It is useful in resolvers, see [following section](#undefined_param_default)
 

## Non-null

Types are assumed to be non-null by default, as in Python typing. Nullable types are obtained using `typing.Optional` (or `typing.Union` with a `None` argument).

!!! note
    There is one exception, when resolver parameter default value is not serializable (and thus cannot be included in the schema), the parameter type is then set as nullable to make the parameter non-required. For example parameters not `Optional` but with `Undefined` default value will be marked as nullable. This is only for the schema, the default value is still used at execution.

## Undefined

In output, `Undefined` is converted to `None`; so in the schema, `Union[T, UndefinedType]` will be nullable.

In input, fields become nullable when `Undefined` is their default value.

## Interfaces

Interfaces are simply classes marked with `apischema.graphql.interface` decorator. An object type implements an interface when its class inherits from an interface-marked class, or when it has [flattened fields](../data_model.md#composition-over-inheritance---composed-dataclasses-flattening) of interface-marked dataclass.

```python
{!interface.py!}
```

## Resolvers

All `dataclass`/`NamedTuple` fields (excepted [skipped](../data_model.md#skip-dataclass-field)) are resolved with their [alias](../json_schema.md#field-alias) in the GraphQL schema.

Custom resolvers can also be added by marking methods with `apischema.graphql.resolver` decorator — resolvers share a common interface with [`apischema.serialized`](../de_serialization.md#serialized-methodsproperties), with a few differences.

Methods can be synchronous or asynchronous (defined with `async def` or annotated with an `typing.Awaitable` return type).

Resolvers parameters are included in the schema with their type, and their default value.

```python
{!resolver.py!}
```

### `GraphQLResolveInfo` parameter

Resolvers can have an additional parameter of type [`graphql.GraphQLResolveInfo`](https://graphql-core-3.readthedocs.io/en/latest/modules/type.html?highlight=GraphQLResolveInfo#graphql.type.GraphQLResolveInfo) (or `Optional[graphql.GraphQLResolveInfo]`), which is automatically injected when the resolver is executed in the context of a GraphQL request. This parameter contains the info about the current GraphQL request being executed.

### Undefined parameter default — `null` vs. `undefined`

`Undefined` can be used as default value of resolver parameters. It can be to distinguish a `null` input from an absent/`undefined` input. In fact, `null` value will result in a `None` argument where no value will use the default value, `Undefined` so.

```python
{!undefined_default.py!}
```

### Error handling

Errors occurring in resolvers can be caught in a dedicated error handler registered with `error_handler` parameter. This function takes in parameters the exception, the object, the [info](#graphqlresolveinfo-parameter) and the *kwargs* of the failing resolver; it can return a new value or raise the current or another exception — it can for example be used to log errors without throwing the complete serialization.

The resulting serialization type will be a `Union` of the normal type and the error handling type; if the error handler always raises, use [`typing.NoReturn`](https://docs.python.org/3/library/typing.html#typing.NoReturn) annotation.

`error_handler=None` correspond to a default handler which only return `None` — exception is thus discarded and the resolver type becomes `Optional`.

The error handler is only executed by *apischema* serialization process, it's not added to the function, so this one can be executed normally and raise an exception in the rest of your code.

Error handler can be synchronous or asynchronous.

```python
{!resolver_error.py!}
```

### Parameters metadata

Resolvers parameters can have metadata like dataclass fields. They can be passed using `typing.Annotated`.

```python
{!resolver_metadata.py!}
```

!!! note
Metadata can also be passed with `parameters_metadata` parameter; it takes a mapping of parameter names as key and mapped metadata as value.

### Parameters base schema

Following the example of [type/field/method base schema](../json_schema.md#base-schema), resolver parameters also support a base schema definition

```python
{!base_schema_parameter.py!}
```

## Scalars

`NewType` or non-object types annotated with `type_name`  will be translated in the GraphQL schema by a `scalar`. By the way, `Any` will automatically be translated to a `JSON` scalar, as it is deserialized from and serialized to JSON.

```python
{!scalar.py!}
```

## ID type
GraphQL `ID` has no precise specification and is defined according API needs; it can be a UUID or/and ObjectId, etc.

`apischema.graphql_schema` has a parameter `id_types` which can be used to define which types will be marked as `ID` in the generated schema. Parameter value can be either a collection of types (each type will then be mapped to `ID` scalar), or a predicate returning if the given type must be marked as `ID`.

```python
{!id_type.py!}
```

!!! note
    `ID` type could also be identified using `typing.Annotated` and a predicate looking into annotations.

*apischema* also provides a simple `ID` type with `apischema.graphql.ID`. It is just defined as a `NewType` of string, so you can use it when you want to manipulate raw `ID` strings in your resolvers.


### ID encoding

`ID` encoding can directly be controlled the `id_encoding` parameters of `graphql_schema`. A current practice is to use *base64* encoding for `ID`.

```python
{!id_conversion.py!}
```

!!! note
    You can also use `relay.base64_encoding` (see [next section](relay.md#id-encoding))

!!! note
    `ID` serialization (respectively deserialization) is applied **after** *apischema* conversions (respectively before *apischema* conversion): in the example, uuid is already converted into string before being passed to `id_serializer`.

    If you use base64 encodeing and an ID type which is converted by *apischema* to a base64 str, you will get a double encoded base64 string


## Tagged unions

!!! important
    This feature has a provisional status, as the concerned [GraphQL RFC](https://github.com/graphql/graphql-spec/pull/733) is not finalized.

*apischema* provides a `apischema.tagged_unions.TaggedUnion` base class which helps to implement the *tagged union* pattern.
It's fields **must** be typed using `apischema.tagged_unions.Tagged` generic type.

```python
{!tagged_union.py!}
```

### JSON schema

Tagged unions JSON schema uses `minProperties: 1` and `maxProperties: 1`.

```python
{!tagged_union_json_schema.py!}
```

### GraphQL schema

As tagged unions are not (yet?) part of the GraphQL spec, they are just implemented as normal (input) object type with nullable fields. An error is raised if several tags are passed in input.

```python
{!tagged_union_graphql_schema.py!}
```

## FAQ

#### Why `TypedDict` is not supported as an output type?

At first, `TypedDict` subclasses are not real classes, so they cannot be used to check types at runtime. Runtime check is however requried to disambiguate unions/interfaces. A hack could be done to solve this issue, but there is another one which cannot be hacked: `TypedDict` inheritance hierarchy is lost at runtime, so they don't play nicely with the interface concept.