File: validation.md

package info (click to toggle)
strawberry-graphql-django 0.78.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,624 kB
  • sloc: python: 31,895; makefile: 24; sh: 21
file content (329 lines) | stat: -rw-r--r-- 9,517 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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
---
title: Validation
---

# Validation

Strawberry Django integrates with Django's validation system to automatically validate GraphQL inputs using Django's model validation, field validators, and forms.

## Overview

Validation in Strawberry Django happens automatically when using `handle_django_errors=True` in mutations. The system calls Django's `Model.full_clean()` before saving, which validates:

- Field-level constraints and validators
- Model-level validation in `clean()` methods
- Unique constraints

For complete details on Django validation, see the [Django Validators documentation](https://docs.djangoproject.com/en/stable/ref/validators/).

## Automatic Validation

Use `handle_django_errors=True` to enable automatic validation:

```python
import strawberry
import strawberry_django
from strawberry_django import mutations

@strawberry_django.input(models.User)
class UserInput:
    email: auto
    username: auto
    age: auto

@strawberry.type
class Mutation:
    create_user: User = mutations.create(
        UserInput,
        handle_django_errors=True  # Automatically validates
    )
```

## Controlling Validation with full_clean

By default, mutations call `full_clean()` before saving. You can control this behavior:

### Disable Validation

```python
@strawberry.type
class Mutation:
    # Skip validation entirely
    create_user: User = mutations.create(UserInput, full_clean=False)
```

### Customize Validation with FullCleanOptions

Use `FullCleanOptions` to control what `full_clean()` validates:

```python
from strawberry_django.mutations.types import FullCleanOptions

@strawberry.type
class Mutation:
    create_user: User = mutations.create(
        UserInput,
        full_clean=FullCleanOptions(
            exclude=["field_to_skip"],      # Fields to exclude from validation
            validate_unique=True,           # Check unique constraints (default: True)
            validate_constraints=True,      # Check model constraints (default: True)
        ),
    )
```

| Option                 | Type        | Default | Description                             |
| ---------------------- | ----------- | ------- | --------------------------------------- |
| `exclude`              | `list[str]` | `[]`    | Fields to exclude from validation       |
| `validate_unique`      | `bool`      | `True`  | Whether to run unique constraint checks |
| `validate_constraints` | `bool`      | `True`  | Whether to run model constraint checks  |

This is useful when:

- Some fields are set programmatically after initial validation
- You want to skip unique checks for performance (and handle IntegrityError separately)
- Certain validation rules don't apply in the GraphQL context

When validation fails, errors are returned in the GraphQL response:

```graphql
mutation {
  createUser(data: { email: "invalid", age: 15 }) {
    ... on User {
      id
      email
    }
    ... on OperationInfo {
      messages {
        field
        message
        kind
      }
    }
  }
}
```

Response with validation errors:

```json
{
  "data": {
    "createUser": {
      "messages": [
        {
          "field": "email",
          "message": "Enter a valid email address",
          "kind": "VALIDATION"
        },
        {
          "field": "age",
          "message": "Users must be at least 18 years old",
          "kind": "VALIDATION"
        }
      ]
    }
  }
}
```

## Model Validation

Define validation logic in your Django models using the `clean()` method:

```python
from django.db import models
from django.core.exceptions import ValidationError

class User(models.Model):
    email = models.EmailField(unique=True)
    age = models.IntegerField()
    username = models.CharField(max_length=50)

    def clean(self):
        """Custom model validation"""
        super().clean()  # Always call parent first
        errors = {}

        if self.age < 18:
            errors['age'] = "Must be at least 18 years old"

        if self.username and len(self.username) < 3:
            errors['username'] = "Must be at least 3 characters"

        if errors:
            raise ValidationError(errors)
```

This validation runs automatically when using `handle_django_errors=True`. See [Django Model Validation](https://docs.djangoproject.com/en/stable/ref/models/instances/#validating-objects) for more details.

## Field Validators

Django field validators work automatically with Strawberry Django:

```python
from django.db import models
from django.core.validators import MinValueValidator, RegexValidator

class Product(models.Model):
    price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        validators=[MinValueValidator(0)]
    )
    sku = models.CharField(
        max_length=20,
        validators=[RegexValidator(r'^[A-Z0-9-]+$')]
    )
```

See [Django Validators](https://docs.djangoproject.com/en/stable/ref/validators/) for built-in validators and how to create custom validators.

## Custom Mutation Validation

For custom validation logic in mutations, raise `ValidationError` with field-specific errors:

```python
import strawberry
import strawberry_django
from django.core.exceptions import ValidationError

@strawberry.type
class Mutation:
    @strawberry_django.mutation(handle_django_errors=True)
    def create_user(self, email: str, age: int) -> User:
        errors = {}

        if not email or "@" not in email:
            errors["email"] = "Invalid email address"

        if age < 18:
            errors["age"] = "Must be at least 18 years old"

        if errors:
            raise ValidationError(errors)

        return models.User.objects.create(email=email, age=age)
```

## Async Validation

For async mutations, use Django's async ORM methods (Django 4.1+):

```python
import strawberry
from django.core.exceptions import ValidationError

@strawberry.type
class Mutation:
    @strawberry.mutation
    async def create_article(self, title: str) -> Article:
        # Check for duplicate using async
        exists = await models.Article.objects.filter(title=title).aexists()

        if exists:
            raise ValidationError({'title': "Article with this title already exists"})

        return await models.Article.objects.acreate(title=title)
```

For older Django versions, wrap ORM calls in `sync_to_async`. See [Django Async documentation](https://docs.djangoproject.com/en/stable/topics/async/) for details.

## Form Validation

Integrate Django forms for complex validation:

```python
import strawberry
import strawberry_django
from django import forms
from django.core.exceptions import ValidationError

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['email', 'username', 'age']

    def clean_username(self):
        username = self.cleaned_data['username']
        if User.objects.filter(username__iexact=username).exists():
            raise ValidationError("Username already taken")
        return username

@strawberry.type
class Mutation:
    @strawberry_django.mutation
    def create_user(self, data: UserInput) -> User:
        form = UserForm({'email': data.email, 'username': data.username, 'age': data.age})

        if not form.is_valid():
            # Convert form errors to ValidationError
            error_dict = {field: error_list[0] for field, error_list in form.errors.items()}
            raise ValidationError(error_dict)

        return form.save()
```

See [Django Forms documentation](https://docs.djangoproject.com/en/stable/topics/forms/) for more on form validation.

## Best Practices

1. **Always use `handle_django_errors=True`** in mutations to enable automatic validation

2. **Put business logic in `Model.clean()`** instead of scattering it across resolvers

3. **Use dict-style ValidationError** for field-specific errors:

   ```python
   raise ValidationError({'field': 'Error message'})
   ```

4. **Test validation** using the test client:
   ```python
   def test_validation(db):
       client = TestClient("/graphql")
       res = client.query("""
           mutation {
               createUser(data: { email: "invalid", age: 15 }) {
                   ... on OperationInfo {
                       messages { field message }
                   }
               }
           }
       """)
       assert res.data["createUser"]["messages"]
   ```

## Common Issues

### Validation Not Running

If validation isn't running automatically, ensure:

1. You're using `handle_django_errors=True`
2. You're using mutation generators or calling `full_clean()` manually

```python
# ✅ Validation runs automatically
create_user: User = mutations.create(UserInput, handle_django_errors=True)

# ❌ Validation doesn't run
user = User.objects.create(email='invalid')  # Bypasses validation
```

### Unique Constraint Errors

Unique constraints raise `IntegrityError` instead of `ValidationError`. Validate in `clean()` to convert to field errors:

```python
def clean(self):
    if User.objects.filter(email=self.email).exclude(pk=self.pk).exists():
        raise ValidationError({'email': "Email already exists"})
```

## See Also

- [Django Model Validation](https://docs.djangoproject.com/en/stable/ref/models/instances/#validating-objects)
- [Django Validators](https://docs.djangoproject.com/en/stable/ref/validators/)
- [Django Forms](https://docs.djangoproject.com/en/stable/topics/forms/)
- [Error Handling](./error-handling.md) - Comprehensive error handling guide
- [Mutations](./mutations.md) - Mutation basics