Field Reference¶
How each Django field is converted into a Pydantic annotation.
Scalar fields¶
| Django field | Python type | Notes |
|---|---|---|
CharField |
str |
max_length becomes a MaxLen constraint |
TextField |
str |
|
SlugField |
str |
|
FileField, FilePathField |
str |
A FieldFile serializes to its .url |
EmailField |
EmailStr |
Requires pydantic[email] |
URLField |
AnyUrl |
|
UUIDField |
UUID |
|
AutoField, IntegerField, SmallIntegerField, BigIntegerField, PositiveIntegerField, PositiveSmallIntegerField |
int |
|
FloatField |
float |
|
DecimalField |
Decimal |
|
BooleanField, NullBooleanField |
bool |
|
DateTimeField |
datetime.datetime |
|
DateField |
datetime.date |
|
TimeField |
datetime.time |
|
DurationField |
datetime.timedelta |
|
BinaryField |
bytes |
|
IPAddressField, GenericIPAddressField |
IPvAnyAddress |
|
JSONField |
Json |
Django 3.2+ |
Any field with choices |
Generated Enum |
See Django Choices |
PostgreSQL fields¶
| Django field | Python type |
|---|---|
ArrayField |
list[<base field type>] |
HStoreField |
Json |
JSONField (django.contrib.postgres) |
Json |
RangeField |
list[<base field type>] |
Verified against a real model¶
class SpeakerProfile(models.Model):
"""Exercises every scalar field conversion documented in the field reference."""
uuid = models.UUIDField()
full_name = models.CharField(max_length=120)
biography = models.TextField(blank=True)
slug = models.SlugField()
email = models.EmailField()
website = models.URLField()
talks_given = models.IntegerField(default=0)
rating = models.FloatField(null=True)
fee = models.DecimalField(max_digits=8, decimal_places=2, null=True)
is_active = models.BooleanField(default=True)
joined_at = models.DateTimeField(null=True)
birth_date = models.DateField(null=True)
preferred_slot = models.TimeField(null=True)
session_length = models.DurationField(null=True)
last_login_ip = models.GenericIPAddressField(null=True)
metadata = models.JSONField(null=True)
class Meta:
app_label = 'examples'
>>> class SpeakerProfileSchema(ModelSchema):
... class Config:
... model = models.SpeakerProfile
>>> for name, info in SpeakerProfileSchema.model_fields.items():
... print(f'{name:16} {info.annotation}')
id typing.Optional[int]
uuid <class 'uuid.UUID'>
full_name <class 'str'>
biography typing.Optional[str]
slug <class 'str'>
email <class 'pydantic.networks.EmailStr'>
website <class 'pydantic.networks.AnyUrl'>
talks_given <class 'int'>
rating typing.Optional[float]
fee typing.Optional[decimal.Decimal]
is_active <class 'bool'>
joined_at typing.Optional[datetime.datetime]
birth_date typing.Optional[datetime.date]
preferred_slot typing.Optional[datetime.time]
session_length typing.Optional[datetime.timedelta]
last_login_ip typing.Optional[pydantic.networks.IPvAnyAddress]
metadata typing.Optional[pydantic.types.Json]
Relation fields¶
| Django field | depth = 0 |
depth ≥ 1 |
|---|---|---|
ForeignKey, OneToOneField |
int (related pk), alias <name>_id |
Nested schema |
ManyToManyField |
list[int] of related pks |
list[NestedSchema] |
OneToOneRel (reverse) |
int |
Nested schema |
ManyToOneRel (reverse FK) |
Not generated | Not generated |
ManyToManyRel (reverse M2M) |
Not generated | Not generated |
Reverse ForeignKey and reverse ManyToMany are skipped when fields are
collected, so a schema never silently acquires a field that triggers a query.
Declare them with Source.
Relations are written through <name>_id
create() and update() detect relation fields and hand the ORM the
<name>_id attribute, which is the form Django accepts for a primary key.
Many-to-many values are applied after the instance is saved, since they need
a pk to attach to.
Relations are read as the related primary key
A relation field accepts either the related instance or its primary key, so reading a Django object and accepting a pk from a JSON payload both work:
What makes a field optional¶
Three independent rules can relax a generated field. Any one of them is enough.
| Rule | Effect |
|---|---|
null=True on the Django field |
Type becomes Optional[...], default None |
blank=True on the Django field |
Type becomes Optional[...], default None |
Listed in Config.optional |
Type becomes Optional[...], default None |
| Is the primary key | Relaxed unless named in fields — see the pk rule |
blank=True is worth calling out: it is a form-level flag in Django, not a
database one, but it does relax the schema field.
>>> SpeakerProfileSchema.model_fields['biography'].is_required() # blank=True
False
>>> SpeakerProfileSchema.model_fields['rating'].is_required() # null=True
False
>>> SpeakerProfileSchema.model_fields['full_name'].is_required() # neither
True
Defaults¶
A Django default becomes the Pydantic default. A callable default becomes a
default_factory.
>>> SpeakerProfileSchema.model_fields['talks_given'].default
0
>>> SpeakerProfileSchema.model_fields['is_active'].default
True
Metadata carried across¶
verbose_name becomes the field title, help_text (falling back to
verbose_name) becomes the description, and max_length becomes a constraint:
>>> info = SpeakerProfileSchema.model_fields['full_name']
>>> info.title
'Full Name'
>>> info.metadata
[MaxLen(max_length=120)]
All three surface in the JSON Schema:
>>> print(json.dumps(SpeakerProfileSchema.model_json_schema()['properties']['full_name'], indent=2))
{
"description": "",
"maxLength": 120,
"title": "Full Name",
"type": "string"
}
max_length is dropped for custom types
Fields converted to a non-str Pydantic type — EmailStr, AnyUrl, and
any choices enum — do not carry max_length, because the constraint does
not apply to the converted type.
Unsupported fields¶
A field with no registered converter raises at class-creation time rather than producing a silently wrong schema:
Handle it by excluding the field, or by declaring the annotation yourself.
Related pages¶
- Django Choices — enum generation in detail
- Relations — relation fields and
depth - ModelSchema configuration — every
Configoption