python
46 lines · 7 steps
Building a deconstructible validator in Django
A reusable, migration-safe validator class checks phone numbers against the E.164 format and attaches to a model field.
Explained by
highlit
1import re
2
3from django.core.exceptions import ValidationError
4from django.core.validators import RegexValidator
5from django.db import models
6from django.utils.deconstruct import deconstructible
7from django.utils.translation import gettext_lazy as _
8
9
10@deconstructible
11class E164Validator:
12 message = _("Enter a valid phone number in E.164 format, e.g. +14155552671.")
13 code = "invalid_phone"
14 pattern = re.compile(r"^\+[1-9]\d{7,14}$")
15
16 def __init__(self, message=None, code=None):
17 if message is not None:
18 self.message = message
19 if code is not None:
20 self.code = code
21
22 def __call__(self, value):
23 normalized = re.sub(r"[\s\-().]", "", str(value))
24 if not self.pattern.match(normalized):
25 raise ValidationError(self.message, code=self.code, params={"value": value})
26
27 def __eq__(self, other):
28 return (
29 isinstance(other, E164Validator)
30 and self.message == other.message
31 and self.code == other.code
32 )
33
34
35class Contact(models.Model):
36 name = models.CharField(max_length=120)
37 phone = models.CharField(
38 _("phone number"),
39 max_length=16,
40 validators=[E164Validator()],
41 help_text=_("International format, e.g. +14155552671."),
42 )
43
44 def clean(self):
45 super().clean()
46 self.phone = re.sub(r"[\s\-().]", "", self.phone)
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Marking a validator @deconstructible lets Django serialize it into migrations by reconstructing the instance from its arguments.
- 2Defining __call__ turns a class instance into a callable validator that Django invokes like a plain function.
- 3Normalizing input before matching keeps the regex strict while tolerating formatting the user typed.
Related explainers
python
from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, ConfigDict, EmailStr from sqlalchemy.orm import Session
Building a users router in FastAPI
routing
serialization
dependency-injection
Intermediate
7 steps
python
import json from dataclasses import dataclass, field, asdict from datetime import datetime from typing import Any
JSON round-tripping with Python dataclasses
dataclasses
serialization
json
Intermediate
6 steps
rust
use std::path::PathBuf; use clap::{Parser, Subcommand, ValueEnum};
Building a CLI with clap's derive macros
cli
derive-macros
argument-parsing
Intermediate
10 steps
python
from dataclasses import dataclass from typing import Optional from sqlalchemy import select, func
Building a pagination helper with SQLAlchemy
pagination
dataclass
computed-properties
Intermediate
8 steps
php
<?php declare(strict_types=1);
A single-action JSON user controller in PHP
invokable-class
json-api
validation
Intermediate
7 steps
python
from datetime import timedelta from django.contrib.sessions.models import Session from django.core.management.base import BaseCommand
A batched session-cleanup command in Django
management-command
batch-processing
database-cleanup
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-deconstructible-validator-in-django-explained-python-0ad1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.