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 fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
ruby
class UserAgentParser BROWSERS = [ [/Edg\/([\d.]+)/, "Edge"], [/OPR\/([\d.]+)/, "Opera"],
Parsing user-agent strings in Ruby
regex
pattern-matching
lookup-tables
Intermediate
8 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 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.