Skip to content

Commit

Permalink
Add experimental support for Pydantic (#453)
Browse files Browse the repository at this point in the history
PR: #453

* Add very rough WIP pydantic support

* Add some docs

* Add support for more types

* Add basic conversion to pydantic

* Force users to specify fields to copy over from pydantic

* Move exceptions in exceptions.py

* Implement `error_type` generation from model

* Move field in a separate file

* Use asdict for to_pydantic

* Move pydantic into beta namespace

* Remove docs about WIP features

* Add tests for the fields argument

* Throw an error if pydantic fields is empty

* Update error_type documentation

* Remove type prefix from pydantic doc

* Add pydantic to dev-dependencies for tests

* Add support for nested from_pydantic

* Add support for additional fields in the strawberry type

* Add documentation about extending types

* Extend pydantic doc about conversion

* Remove code from first WIP implementation

* Move pydantic in integrations and improve docs

* Test printed schema

* Mark pydantic as experimental

* Add support for Pydantic Field alias

* Try to fix mypy

* Improve docs

* Nitpicky changes

Co-authored-by: Marco Acierno <marcoaciernoemail@gmail.com>
Co-authored-by: ignormies <bryce.beagle@gmail.com>
  • Loading branch information
3 people committed Nov 18, 2020
1 parent 9879998 commit c83f100
Show file tree
Hide file tree
Showing 26 changed files with 2,047 additions and 56 deletions.
27 changes: 27 additions & 0 deletions RELEASE.md
@@ -0,0 +1,27 @@
Release type: minor

This release adds support for creating types from Pydantic models. Here's an
example:

```python
import strawberry

from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class UserModel(BaseModel):
id: int
name = 'John Doe'
signup_ts: Optional[datetime] = None
friends: List[int] = []

@strawberry.experimental.pydantic.type(model=UserModel, fields=[
'id',
'name',
'friends'
])
class UserType:
pass
```
228 changes: 228 additions & 0 deletions docs/05_integrations/pydantic.md
@@ -0,0 +1,228 @@
---
title: Pydantic
path: /docs/integrations/pydantic
experimental: true
---

# Pydantic support

Strawberry comes with support for
[Pydantic](https://pydantic-docs.helpmanual.io/). This allows for the creation
of Strawberry types from pydantic models without having to write code twice.

Here's a basic example of how this works, let's say we have a pydantic Model for
a user, like this:

```python
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
id: int
name: str
signup_ts: Optional[datetime] = None
friends: List[int] = []
```

We can create a Strawberry type by using the
`strawberry.experimental.pydantic.type` decorator:

```python
import strawberry

from .models import User

@strawberry.experimental.pydantic.type(model=User, fields=[
'id',
'name',
'friends'
])
class User:
pass
```

The `strawberry.experimental.pydantic.type` decorator accepts a Pydantic model
and a list of fields that we want to expose on our GraphQL API.

> **Note** specifying the list of field is required to prevent accidentally
> exposing fields that weren't meant to be exposed on a API
## Input types

Input types are similar to types; we can create one by using the
`strawberry.experimental.pydantic.input` decorator:

```python
import strawberry

from .models import User

@strawberry.experimental.pydantic.input(model=User, fields=[
'id',
'name',
'friends'
])
class UserInput:
pass
```

## Error Types

In addition to object types and input types, Strawberry allows you to create
"error types". You can use these error types to have a typed representation of
Pydantic errors in GraphQL. Let's see an example:

```python+schema
import pydantic
import strawberry
class User(BaseModel):
id: int
name: pydantic.constr(min_length=2)
signup_ts: Optional[datetime] = None
friends: List[int] = []
@strawberry.experimental.pydantic.error_type(model=User, fields=[
'id',
'name',
'friends'
])
class UserError:
pass
---
type UserError {
id: [String!]
name: [String!]
friends: [[String!]]
}
```

where each field will hold a list of error messages

### Extending types

You can use the usual Strawberry syntax to add additional new fields to the
GraphQL type that aren't defined in the pydantic model

```python+schema
import strawberry
import pydantic
from .models import User
class User(BaseModel):
id: int
name: str
@strawberry.experimental.pydantic.type(model=User, fields=[
'id',
'name',
])
class User:
age: int
---
type User {
id: Int!
name: String!
age: Int!
}
```

### Converting types

The generated types won't run any pydantic validation. This is to prevent
confusion when extending types and also to be able to run validation exactly
where it is needed.

To convert a Pydantic instance to a Strawberry instance you can use
`from_pydantic` on the Strawberry type:

```python
import strawberry
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
id: int
name: str


@strawberry.experimental.pydantic.type(model=User, fields=[
'id',
'name',
])
class User:
pass

instance = User(id='123', name='Jake')

data = UserType.from_pydantic(instance)
```

If your Strawberry type includes additional fields that aren't defined in the
pydantic model, you will need to use the `extra` parameter of `from_pydantic` to
specify the values to assign to them.

```python
import strawberry
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
id: int
name: str


@strawberry.experimental.pydantic.type(model=User, fields=[
'id',
'name',
])
class User:
age: int

instance = User(id='123', name='Jake')

data = UserType.from_pydantic(instance, extra={'age': 10})
```

The data dictionary structure follows the structure of your data -- if you have
a list of `User`, you should send an `extra` that is the list of `User` with the
missing data (in this case, `age`).

You don't need to send all fields; data from the model is used first and then
the `extra` parameter is used to fill in any additional missing data.

To convert a Strawberry instance to a pydantic instance and trigger validation,
you can use `to_pydantic` on the Strawberry instance:

```python
import strawberry
from typing import List, Optional
from pydantic import BaseModel


class User(BaseModel):
id: int
name: str


@strawberry.experimental.pydantic.input(model=User, fields=[
'id',
'name',
])
class UserInput:
pass

input_data = UserInput(id='abc', name='Jake')

# this will run pydantic's validation
instance = input_data.to_pydantic()
```
7 changes: 7 additions & 0 deletions mypy_tests.ini
Expand Up @@ -10,3 +10,10 @@ implicit_reexport = False

[mypy-graphql.*]
ignore_errors = True

; Pydantic dependencies
[mypy-email_validator.*]
ignore_missing_imports = True

[mypy-dotenv.*]
ignore_missing_imports = True

0 comments on commit c83f100

Please sign in to comment.