Tuez vos formulaires codés en dur : comment la génération de formulaires pilotée par JSON a transformé ma façon de créer des applications Django
Et si chaque formulaire de votre application Django - création, édition, mise à jour groupée, modale, inline - était défini comme un dictionnaire Python plutôt que comme une classe ? Deux systèmes en production m'ont appris que les formulaires sont des données, pas du code, et cette différence est transformatrice.
La douleur des formulaires codés en dur
Tout développeur Django connaît le rituel. Vous définissez un modèle. Puis vous écrivez un ModelForm. Puis vous ajustez les widgets. Puis vous ajoutez une mise en page crispy. Puis le client dit « ajoutez un champ » et vous touchez à quatre fichiers. Puis il dit « rendez ce champ conditionnel » et vous ajoutez du JavaScript. Puis il dit « le même formulaire, mais dans une fenêtre modale » et vous dupliquez un template.
Multipliez cela par 20 modèles et vous vous retrouvez à maintenir une petite armée de classes de formulaire, chacune légèrement différente, chacune codée en dur, chacune se désynchronisant discrètement du modèle qu'elle représente.
J'ai heurté ce mur sur deux projets très différents - un système de contrôle qualité pour la fabrication industrielle et un back-office de trading financier - et je suis arrivé à la même solution les deux fois : cesser d'écrire des classes de formulaire et commencer à écrire des configurations de formulaire.
L'idée centrale : les formulaires sont des données
Un champ de formulaire Django n'est en réalité qu'un ensemble de propriétés : un nom, un type, un widget, un libellé, s'il est requis, quels choix il propose. Rien dans cette description n'exige une définition de classe Python. Ce sont des données, et les données ont leur place dans une structure de données.
L'idée est simple : définir les champs sous forme de dictionnaires, les stocker dans un schéma, et laisser une unique classe de formulaire générique se construire elle-même à partir de ce schéma à l'exécution.
# Instead of this:
class InspectionForm(forms.Form):
diameter = forms.FloatField(
label="Inner diameter",
widget=forms.NumberInput(attrs={"class": "form-control"}),
)
status = forms.ChoiceField(
choices=[("pass", "Pass"), ("fail", "Fail")],
widget=forms.Select(attrs={"class": "form-select"}),
)
# Write this:
INSPECTION_SCHEMA = {
"fields": [
{
"name": "diameter",
"type": "FloatField",
"widget": "NumberInput",
"label": "Inner diameter",
"widget_attrs": {"class": "form-control"},
},
{
"name": "status",
"type": "ChoiceField",
"widget": "Select",
"label": "Status",
"choices": [("pass", "Pass"), ("fail", "Fail")],
"widget_attrs": {"class": "form-select"},
},
]
}
Le moteur DynamicForm
Tout le système repose sur une seule classe. Elle associe des identifiants sous forme de chaînes à des classes de champs et de widgets Django, puis parcourt le schéma pour se construire elle-même :
class DynamicForm(forms.Form):
FIELD_MAP = {
"CharField": forms.CharField,
"ChoiceField": forms.ChoiceField,
"FloatField": forms.FloatField,
"IntegerField": forms.IntegerField,
"BooleanField": forms.BooleanField,
"DateField": forms.DateField,
"DecimalField": forms.DecimalField,
"EmailField": forms.EmailField,
# ... every Django field type
}
WIDGET_MAP = {
"TextInput": forms.TextInput,
"Select": forms.Select,
"NumberInput": forms.NumberInput,
"Textarea": forms.Textarea,
"CheckboxInput": forms.CheckboxInput,
"DateInput": forms.DateInput,
# ... every Django widget type
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_cfg in self.scheme["fields"]:
field_class = self.FIELD_MAP[field_cfg["type"]]
widget_class = self.WIDGET_MAP[field_cfg["widget"]]
self.fields[field_cfg["name"]] = field_class(
label=field_cfg["label"],
widget=widget_class(
attrs=field_cfg.get("widget_attrs", {})
),
required=field_cfg.get("required", True),
choices=field_cfg.get("choices", []),
)
Voilà tout le moteur. Chaque type de champ Django, chaque type de widget, mappé par recherche de chaîne. Un nouveau formulaire est un nouveau dictionnaire, pas une nouvelle classe.
Étude de cas 1 : contrôle qualité en industrie manufacturière
Le premier projet était un système de données de production pour un fabricant industriel. Les inspecteurs qualité suivent des procédures opératoires normalisées (SOP) qui exigent de mesurer des dimensions précises, de contrôler les traitements de surface et d'enregistrer un résultat conforme/non conforme pour chaque étape de production.
Le défi : plus de 20 formulaires d'inspection différents, chacun avec des champs propres au type de pièce et à l'étape SOP. Une inspection de plaque d'extrémité sur tour vertical mesure des choses différentes d'une inspection de fraisage de plaque bipolaire.
Avec des formulaires codés en dur, cela représente plus de 20 classes de formulaire, chacune avec ses propres définitions de champs, sa propre validation, ses propres particularités de template. Avec l'approche par blueprint, ce sont plus de 20 dictionnaires et une seule classe de formulaire.
# Chaque inspection de contrôle qualité n'est qu'une définition de schéma
QC_ENDPLATE_LATHE_SCHEMA = {
"fields": [
{
"name": "inner_diameter",
"widget": "TextInput",
"label": _("Diamètre intérieur : 1846mm"),
},
{
"name": "outer_diameter",
"widget": "TextInput",
"label": _("Diamètre extérieur : 1876-1876.1mm"),
},
{
"name": "surface_qualified",
"type": "ChoiceField",
"widget": "Select",
"label": _("Qualification de surface"),
"choices": [
("qualified", _("Qualifié")),
("unqualified", _("Non qualifié")),
],
},
]
}
# La classe de formulaire tient en une ligne
class EndPlateLathQCForm(DynamicForm, QCValidationMixin):
scheme = QC_ENDPLATE_LATHE_SCHEMA
Les données collectées sont stockées au format JSON dans un JSONField, ce qui préserve à la fois les libellés et les valeurs des champs. L'enregistrement d'inspection est ainsi auto-descriptif - vous pouvez le lire sans savoir quel schéma l'a généré.
# Données stockées sous forme de JSON auto-descriptif
{
"inner_diameter": {
"label": "Diamètre intérieur : 1846mm",
"value": "1846.02"
},
"surface_qualified": {
"label": "Qualification de surface",
"value": "qualified"
}
}
Étude de cas 2 : back-office de trading financier
Le deuxième projet a poussé le schéma plus loin. Un back-office de trading de cryptomonnaies avait besoin de formulaires pour les comptes, les commissions, les profils de risque, le statut de conformité, les plans tarifaires - chacun avec des champs différents, des permissions différentes, des méthodes de soumission différentes (POST pour la création, PATCH pour les mises à jour en masse), et des contextes de rendu différents (page complète, modale, en ligne).
Ici, le blueprint a dépassé les définitions de champs pour devenir une spécification de formulaire complète :
ACCOUNT_CREATE_BLUEPRINT: FormBlueprint = {
"name": "account_create",
"title": "Create Account",
"column_count": 2,
"modal_size": "xl",
"method": "POST",
"api_config": "account:accounts_v1",
"success_message": "Account created successfully",
"fields": [
{
"name": "currency",
"type": "ChoiceField",
"label": "Currency",
"permission": "ACCOUNT_CURRENCY_UPDATE",
"api_choices": "common:unified_search",
"api_choices_args": ["assets-v1"],
"api_choices_autocomplete": True,
"row_class": "col-md-6",
},
{
"name": "commission_rate",
"type": "FloatField",
"label": "Commission Rate (%)",
"permission": "ACCOUNT_COMMISSION_UPDATE",
"widget_attrs": {"step": "0.01", "min": "0"},
"row_class": "col-md-6",
},
],
}
Remarquez ce qui est encodé dans ce seul dictionnaire : la méthode HTTP, le point de terminaison de l'API, la taille de la modale, quels champs requièrent quelles permissions, où charger les choix des listes déroulantes, la mise en page de la grille Bootstrap, et le message de succès. C'est toute une expérience de formulaire en pures données.
Visibilité des champs pilotée par les permissions
La plateforme financière a ajouté une fonctionnalité puissante : le contrôle des permissions par champ. Chaque champ du blueprint peut déclarer une chaîne de permission. Le moteur de rendu du formulaire vérifie les permissions de l'utilisateur courant et se dégrade avec élégance :
class FormRenderer:
def _apply_field_permissions(self, fields, user):
user_perms = get_user_section_permissions(user)
for field in fields:
required_perm = field.get("permission")
if required_perm and required_perm not in user_perms:
field.setdefault("widget_attrs", {})
field["widget_attrs"]["disabled"] = True
field["widget_attrs"]["readonly"] = True
Les utilisateurs sans la permission ACCOUNT_COMMISSION_UPDATE voient le champ du taux de commission, mais il est en lecture seule et grisé. Pas de template séparé, pas de blocs {% if perms.x %}, pas de logique de rendu conditionnel. Le blueprint déclare la règle, le moteur de rendu l'applique.
Choix chargés via API et autocomplétion
Les listes de choix statiques conviennent aux menus déroulants « Qualifié/Non qualifié », mais un sélecteur de devise doit puiser dans une API en direct. Le blueprint gère cela avec deux propriétés :
api_choices- un nom d'URL Django qui renvoie des choixapi_choices_autocomplete- active Select2 avec une recherche côté serveur
Le moteur de formulaires résout le nom d'URL, récupère les choix au moment du rendu et met en place l'autocomplétion Select2 si elle est configurée. L'auteur du blueprint n'écrit jamais de JavaScript - il déclare simplement où résident les données.
Pourquoi c'est mieux que des formulaires codés en dur
Après avoir construit deux grandes applications avec ce pattern, je peux formuler précisément ce qu'il vous apporte :
- Ajouter un formulaire, c'est ajouter des données, pas du code. Une nouvelle inspection QC ou un nouveau type de compte est un nouveau dictionnaire. Pas de nouvelle classe, pas de nouveau template, pas de nouvelle URL. Le système le découvre et le rend.
- Modifier un formulaire, c'est modifier un seul endroit. Un libellé de champ erroné ? Modifiez le dictionnaire. Besoin d'une nouvelle option de liste déroulante ? Mettez à jour la liste des choix. Besoin de rendre un champ facultatif ? Définissez
"required": False. Un fichier, un changement, terminé. - Les formulaires deviennent portables. Un blueprint n'est qu'un dictionnaire. Vous pouvez le sérialiser en JSON, le stocker dans une base de données, le charger depuis un fichier de configuration ou l'envoyer via une API. Le moteur de formulaires se moque de la provenance du schéma.
- La cohérence est automatique. Chaque formulaire passe par le même pipeline de rendu. Les attributs de widget, les classes CSS, le comportement de mise en page - tout est standardisé par le moteur, et non copié-collé entre des classes de formulaire.
- Un stockage de données auto-descriptif. Lorsque les données de formulaire sont enregistrées en JSON avec libellés et valeurs, l'enregistrement stocké est lisible par un humain sans référence au schéma. Les rapports et les exports deviennent triviaux.
- Les mixins de validation se composent proprement. Le formulaire dynamique reste un formulaire Django. Vous pouvez ajouter des mixins pour une validation propre au domaine (vérifications d'existence de pièce, validation de permissions) sans toucher au schéma.
Quand recourir à ce pattern
Les formulaires dynamiques ne sont pas toujours le bon choix. Pour un formulaire de connexion à deux champs, un ModelForm est plus simple et plus clair. Mais le motif brille lorsque :
- Vous avez de nombreux formulaires similaires avec des ensembles de champs différents (inspections qualité, étapes d'intégration, pages de sondage)
- Les formulaires doivent être configurables sans modifications de code - par des administrateurs, des clients ou des systèmes externes
- Vous avez besoin d'un contrôle des champs piloté par les permissions sur de nombreux formulaires sans dupliquer la logique de template
- Les données de formulaire doivent être stockées sous forme de JSON auto-descriptif plutôt que dans des colonnes de base de données fixes
- Vous construisez une plateforme multi-locataires dont chacun a besoin de formulaires légèrement différents
Le coût initial est d'une classe DynamicForm (une cinquantaine de lignes) et de la discipline de définir des schémas plutôt que des classes de formulaire. Le bénéfice est un système où ajouter un 21e formulaire demande le même effort que d'ajouter le 2e.
Un formulaire codé en dur est un instantané des besoins d'aujourd'hui. Un schéma de formulaire est un contrat qui évolue. Écrivez le contrat.