Skip to content

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

models.py
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.

>>> class EventSchema(ModelSchema):
...     class Config:
...         model = models.Event
>>> category = models.Category.objects.create(name='Python')
>>> EventSchema.model_validate({'title': 'DjangoCon', 'category': category.pk}).create().category_id
1

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:

>>> EventSchema.model_validate({'title': 'DjangoCon', 'category': category}).category
1
>>> EventSchema.model_validate({'title': 'DjangoCon', 'category': category.pk}).category
1

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.

>>> SpeakerProfileSchema.model_fields['email'].metadata
[]

Unsupported fields

A field with no registered converter raises at class-creation time rather than producing a silently wrong schema:

Exception: Don't know how to convert the Django field <field> (<class>)

Handle it by excluding the field, or by declaring the annotation yourself.