Skip to main content

Tools to convert Django ORM models to Pydantic models

Project description

dantico

Tools to convert Django ORM models to Pydantic models.

GitHub Actions (Test) Codecov PyPI version Downloads PyPI Supported Python Versions PyPI Supported Django Versions

The key features are:

  • Custom Field Support: Create Pydantic Schemas from Django Models with default field type validations out of the box.

  • Field Validator: Fields can be validated with Pydantic validator or root_validator.

Requirements

  • Python 3.6+
  • Django 2.2+
  • Pydantic 1.6+

Installation

pip install dantico

Usage

Here are a few examples of what you can do with dantico:

Basic

Assume we have the following user model definition:

# models.py

from django.db import models


class User(models.Model):

    GENDER_MALE = "male"
    GENDER_FEMALE = "female"
    GENDER_OTHER = "other"

    GENDER_CHOICES = (
        (GENDER_MALE, "Male"),
        (GENDER_FEMALE, "Female"),
        (GENDER_OTHER, "Other"),
    )

    username = models.CharField(max_length=20)
    age = models.IntegerField()
    gender = models.CharField(
        choices=GENDER_CHOICES,
        max_length=10,
        blank=True,
    )
    password = models.CharField(max_length=100)
    company = models.ForeignKey(
        Company,
        on_delete=models.CASCADE,
    )
    languages = models.ManyToManyField(Language)

    def __str__(self):
        return self.name

Using the ModelSchema class will automatically generate schemas from our User model.

# schemas.py

from dantico import ModelSchema
from users.models import User


class UserSchema(ModelSchema):
    class Config:
        model = User


json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)


# Output:
{
    "title": "UserSchema",
    "type": "object",
    "properties": {
        "id": {
            "title": "Id",
            "extra": {},
            "type": "integer"
        },
        "Username": {
            "title": "Username",
            "maxLength": 20,
            "type": "string"
        },
        "age": {
            "title": "Age",
            "type": "integer"
        },
        "gender": {
            "title": "Gender",
            "allOf": [
                {
                    "$ref": "#/definitions/GenderEnum"
                }
            ]
        },
        "password": {
            "title": "Password",
            "maxLength": 100,
            "type": "string"
        },
        "company_id": {
            "title": "Company",
            "type": "integer"
        },
        "languages": {
            "title": "Languages",
            "type": "array",
            "items": {
                "type": "integer"
            }
        }
    },
    "required": [
        "Username",
        "age",
        "password",
        "company_id",
        "languages"
    ],
    "definitions": {
        "GenderEnum": {
            "title": "GenderEnum",
            "description": "An enumeration.",
            "enum": [
                "male",
                "female",
                "other"
            ]
        }
    }
}

Excluding and including model fields

By default dantico include all the fields from the Django model. As a rule of thumb, always use the include or exclude attribute to explicitly define a list of fields that you want to be visible in your API. Note that you cannot use both at the same time.

# schemas.py

from dantico import ModelSchema
from users.models import User


class UserSchema(ModelSchema):
    class Config:
        model = User
        exclude = ["password", "age"]


json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)


# Output:
{
    "title": "UserSchema",
    "type": "object",
    "properties": {
        "id": {
            "title": "Id",
            "extra": {},
            "type": "integer"
        },
        "username": {
            "title": "Username",
            "maxLength": 20,
            "type": "string"
        },
        "gender": {
            "title": "Gender",
            "allOf": [
                {
                    "$ref": "#/definitions/GenderEnum"
                }
            ]
        },
        "company_id": {
            "title": "Company",
            "type": "integer"
        },
        "languages": {
            "title": "Languages",
            "type": "array",
            "items": {
                "type": "integer"
            }
        }
    },
    "required": [
        "username",
        "company_id",
        "languages"
    ],
    "definitions": {
        "GenderEnum": {
            "title": "GenderEnum",
            "description": "An enumeration.",
            "enum": [
                "male",
                "female",
                "other"
            ]
        }
    }
}

An example of using include:

# schemas.py

from dantico import ModelSchema
from users.models import User


class UserSchema(ModelSchema):
    class Config:
        model = User
        include = ["username", "age", "company"]


json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)


# Output:
{
    "title": "UserSchema",
    "type": "object",
    "properties": {
        "username": {
            "title": "Username",
            "maxLength": 20,
            "type": "string"
        },
        "age": {
            "title": "Age",
            "type": "integer"
        },
        "company_id": {
            "title": "Company",
            "type": "integer"
        }
    },
    "required": [
        "Username",
        "age",
        "company_id"
    ]
}

Optional model fields

We can also specify model fields to mark as optional.

# schemas.py

from dantico import ModelSchema
from users.models import User


class UserSchema(ModelSchema):
    class Config:
        model = User
        exclude = ["password", "languages"]
        optional = ["age"] # 'age' schema field is now optional


json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)


# Output:
{
    "title": "UserSchema",
    "type": "object",
    "properties": {
        "id": {
            "title": "Id",
            "extra": {},
            "type": "integer"
        },
        "username": {
            "title": "Username",
            "maxLength": 20,
            "type": "string"
        },
        "age": {
            "title": "Age",
            "extra": {},
            "type": "integer"
        },
        "gender": {
            "title": "Gender",
            "allOf": [
                {
                    "$ref": "#/definitions/GenderEnum"
                }
            ]
        },
        "company_id": {
            "title": "Company",
            "type": "integer"
        }
    },
    "required": [
        "username",
        "company_id"
    ],
    "definitions": {
        "GenderEnum": {
            "title": "GenderEnum",
            "description": "An enumeration.",
            "enum": [
                "male",
                "female",
                "other"
            ]
        }
    }
}

Introspect the related objects

The depth attribute lets us look into the Django model relations (many to one, one to one, many to many).

Consider the following models definitions:

# models.py

from django.db import models


class Company(models.Model):
    name = models.CharField(max_length=20)
    location = models.CharField(max_length=20)
    date_created = models.DateField()

    def __str__(self):
        return self.name


class Language(models.Model):

    name = models.CharField(max_length=20)
    creator = models.CharField(max_length=20)
    paradigm = models.CharField(max_length=20)
    date_created = models.DateField()

    def __str__(self):
        return self.name


class User(models.Model):

    GENDER_MALE = "male"
    GENDER_FEMALE = "female"
    GENDER_OTHER = "other"

    GENDER_CHOICES = (
        (GENDER_MALE, "Male"),
        (GENDER_FEMALE, "Female"),
        (GENDER_OTHER, "Other"),
    )

    username = models.CharField(max_length=20)
    age = models.IntegerField()
    gender = models.CharField(
        choices=GENDER_CHOICES,
        max_length=10,
        blank=True,
    )
    password = models.CharField(max_length=100)
    company = models.ForeignKey(
        Company,
        on_delete=models.CASCADE,
    )
    languages = models.ManyToManyField(Language)

    def __str__(self):
        return self.name

Now let's add the depth attribute:

# schemas.py

from dantico import ModelSchema
from users.models import User


class UserSchema(ModelSchema):
    class Config:
        model = User
        exclude = ["password", "age", "gender"]
        optional = ["age"]
        depth = 1 # by default, depth = 0


json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)


# Output:
{
    "title": "UserSchema",
    "type": "object",
    "properties": {
        "id": {
            "title": "Id",
            "extra": {},
            "type": "integer"
        },
        "username": {
            "title": "Username",
            "maxLength": 20,
            "type": "string"
        },
        "company": {
            "title": "Company",
            "allOf": [
                {
                    "$ref": "#/definitions/Company"
                }
            ]
        },
        "languages": {
            "title": "Languages",
            "type": "array",
            "items": {
                "$ref": "#/definitions/Language"
            }
        }
    },
    "required": [
        "username",
        "company",
        "languages"
    ],
    "definitions": {
        "Company": {
            "title": "Company",
            "type": "object",
            "properties": {
                "id": {
                    "title": "Id",
                    "extra": {},
                    "type": "integer"
                },
                "name": {
                    "title": "Name",
                    "maxLength": 20,
                    "type": "string"
                },
                "location": {
                    "title": "Location",
                    "maxLength": 20,
                    "type": "string"
                },
                "date_created": {
                    "title": "Date Created",
                    "type": "string",
                    "format": "date"
                }
            },
            "required": [
                "name",
                "location",
                "date_created"
            ]
        },
        "Language": {
            "title": "Language",
            "type": "object",
            "properties": {
                "id": {
                    "title": "Id",
                    "extra": {},
                    "type": "integer"
                },
                "name": {
                    "title": "Name",
                    "maxLength": 20,
                    "type": "string"
                },
                "creator": {
                    "title": "Creator",
                    "maxLength": 20,
                    "type": "string"
                },
                "paradigm": {
                    "title": "Paradigm",
                    "maxLength": 20,
                    "type": "string"
                },
                "date_created": {
                    "title": "Date Created",
                    "type": "string",
                    "format": "date"
                }
            },
            "required": [
                "name",
                "creator",
                "paradigm",
                "date_created"
            ]
        }
    }
}

Schema customization

Docstrings and titles can be used as descriptive text in the schema output.

# schemas.py

from pydantic import Field
from dantico import ModelSchema
from users.models import User


class UserSchema(ModelSchema):
    """My user model schema"""
    username: str = Field(
        title="The user's username",
        description="This is the user's username",
    )
    age: int = Field(
        None,
        title="The user's age",
        description="This is the user's age",
    )

    class Config:
        model = User
        exclude = ["password", "gender", "languages"]
        title = "User schema"


json_output = json.dumps(UserSchema.schema(), indent=4)
print(json_output)


# Output:
{
    "title": "User schema",
    "description": "My user model schema",
    "type": "object",
    "properties": {
        "username": {
            "title": "The user's username",
            "description": "This is the user's username",
            "type": "string"
        },
        "age": {
            "title": "The user's age",
            "description": "This is the user's age",
            "type": "integer"
        },
        "id": {
            "title": "Id",
            "extra": {},
            "type": "integer"
        },
        "company_id": {
            "title": "Company",
            "type": "integer"
        }
    },
    "required": [
        "username",
        "company_id"
    ]
}

Field validator

Custom validation is not an easy task in this case. But we still can achieve it by using threads.

Because we define a validator to validate fields on inheriting models, we should set check_fields=False on the validator. More information can be found here.

# schemas.py

import asyncio
import concurrent.futures

from asgiref.sync import sync_to_async
from dantico import ModelSchema
from pydantic import validator

from users.models import User


class UserSchema(ModelSchema):
    class Config:
        model = User
        exclude = ["password"]

    @validator("username", check_fields=False)
    def validate_username(cls, v):
        """
        Here we are using async method as validator. Because there is
        already an event loop (using FastAPI), we need to start another thread.

        :param cls: Access the class of the object that is being validated
        :param v: Validate the value
        :return: The result of the username_must_be_unique function
        """

        @sync_to_async
        def username_must_be_unique():
            if User.objects.filter(username__icontains=v).exists():
                raise ValueError("username already exists")
            return v

        # A way to run async code in a sync environment.
        pool = concurrent.futures.ThreadPoolExecutor(1)
        result = pool.submit(asyncio.run, username_must_be_unique()).result()

        return result

License

This project is licensed under the terms of the MIT license.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dantico-0.0.7.tar.gz (26.6 kB view hashes)

Uploaded Source

Built Distribution

dantico-0.0.7-py3-none-any.whl (17.2 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page