8

My question is pretty simple and I'm surprised no one has asked it so far:

How can I validate dates in pydantic?

For example, I only want to accept dates in the range 1980.1.1-2000.1.1.

2 Answers 2

9

validator for datetime field is what you want. You can use it as following:

from pydantic import BaseModel, validator
from datetime import datetime

class DModel(BaseModel):
    dt: datetime

    @validator("dt")
    def ensure_date_range(cls, v):
        if not datetime(year=1980, month=1, day=1) <= v < datetime(year=2000, month=1, day=1):
            raise ValueError("Must be in range")
        return v

DModel.parse_obj({"dt": "1995-01-01T00:00"})
DModel.parse_obj({"dt": "2001-01-01T00:00"})  # validation error
Sign up to request clarification or add additional context in comments.

4 Comments

Can I use the same validator for multiple pydantic classes/schemas/models?
Yes, as described here
link seems broken @alex_noname
@validator is deprecated in Pydantic V2
8

Original Pydantic Answer

One common use case, possibly hinted at by the OP's use of "dates" in the plural, is the validation of multiple dates in the same model. This is very lightly documented, and there are other problems that need to be dealt with you want to parse strings in other date formats.

This is not explicitly in the docs. Well, it's there, but the circumstances where it's required are not mentioned. If you need your validator to convert a non-ISO str format to date Pydantic will discover an invalid date format before your validator has run.

In these circumstances it's not sufficient to just apply multiple validators, or apply one validator to multiple fields, the pre=True argument also needs to be supplied, to pre-empt the default formatting from raising an exception, before your validator can convert it:

from datetime import datetime, date
from pydantic import BaseModel, validator


def validate_date(value: str) -> date:
    return datetime.strptime(value, "%d/%m/%Y").date()


class CommonModel(BaseModel):
    created: date
    modified: date
    confirmed: date
    closed: date

    _validate_dates = validator(
        'created', 'modified', 'confirmed', 'closed', pre=True, allow_reuse=True,
    )(validate_date)

data = {
    'created': '25/12/2000',
    'modified': '31/12/2000',
    'confirmed': '01/01/2001',
    'closed': '11/09/2001',
}

try:
    model = CommonModel(**data)
except Exception as e:
    print(e.json())

The docs on this are so light I had to discover this myself, and I haven't found anyone else pointing this out, so I thought a demonstration of this kind of code would be helpful here.


Updated Pydantic 2.3 Answer

Pydantic 2 is better and is now, so in response to @Gibbs' I am updating with a Pydantic 2.3 solution that contains other non-date fields as well.

While Pydantic 2 documentation continues to be a little skimpy the migration to Pydantic 2 is managed, with specific migration documentation identifying some of the changes required and with the new Python 11 helpful error messages it wasn't too hard to discover.

Pydantic 2 also changes the approach a bit, with specific support for multiple fields, making cleaner code so I've used that support in this answer. If you want to use this validator with multiple models with different field names for the dates in those models, then you may have to revert the structure similar to my original answer.

import datetime
import pydantic

class CommonModel(pydantic.BaseModel):
    name: str
    created: datetime.date
    modified: datetime.date
    confirmed: datetime.date
    closed: datetime.date
    number: int

    @pydantic.field_validator(
        "created", "modified", "confirmed", "closed", mode="before"
    )
    @classmethod
    def non_iso_date(
        cls, value: str, info: pydantic.FieldValidationInfo
    ) -> datetime.date:
        assert isinstance(value, str), info.field_name + " must be date as str 'dd/mm/yyyy'"
        return datetime.datetime.strptime(value, "%d/%m/%Y").date()


data = {
    "created": "25/12/2000",
    "modified": "31/12/2000",
    "confirmed": "01/01/2001",
    "closed": "11/09/2001",
    "number": 3,
    "name": "rumplestiltskin",
}

try:
    model = CommonModel(**data)
    print(model)
except Exception as e:
    print(e.json())

This will generate output as follows:

name='rumplestiltskin' created=datetime.date(2000, 12, 25) modified=datetime.date(2000, 12, 31) confirmed=datetime.date(2001, 1, 1) closed=datetime.date(2001, 9, 11) number=3

Pydantic 2 documentation on before and after validators and field validators is all on the same page.

7 Comments

This breaks if I have one non-date field in the model. Do you know how to handle this?
File "pydantic/main.py", line 241, in pydantic.main.ModelMetaclass.__new__ File "pydantic/class_validators.py", line 189, in pydantic.class_validators.ValidatorGroup.check_for_unused pydantic.errors.ConfigError: Validators defined with incorrect fields: validate_date (use check_fields=False if you're inheriting from the model and intended this)
That's funny @Gibbs, I was using this with a ton of other fields. I hope you didn't put the non-date fields in the _validate_dates call? I'm just checking on this but I've moved to Pydantic 2 now so some other things need to change. I'll update my answer if I can.
+1 from my end. Thanks for your efforts. And it's due to incorrect spelling of a field name. This works. I am forced to use pydantic < 2 due to non availability of MetaModelClass in pydatnic2+ stackoverflow.com/questions/67699451/…. I see you commented there in the decorator approach.
I don't know your environment @Gibbs but Pydantic 2 should make everything better and easier, in theory, if your environment can support it. One slight issue with Pydantic 2 is that a lot of the SO solutions are Pydantic 1, but could be migratable I guess. If you're looking at making all fields optional there are some helpful answers around and there is my other question around that requirement too.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.