ADR-013 — Politique nullable / required dans les contrats JSON Forge¶
Statut¶
Acceptée — Forge 3.x (ticket NULLABLE-CONTRACT-002-DECIDE-NULLABLE-REQUIRED-RULE).
Date¶
2026-05-19
Contexte¶
L'audit NULLABLE-CONTRACT-001 a révélé trois incohérences dans le comportement de nullable
et required entre fields[] d'entité et pivot.fields[] :
- Défaut absent : entité →
NOT NULL; pivot →NULL;field.schema.jsonditdefault: true. required: falsesansnullable: entité →NOT NULL; pivot →NULL.required: true + nullable: true: entité →NULL(nullablegagne) ; pivot →NOT NULL(requiredgagne).
Ces incohérences proviennent d'une règle implicite jamais formalisée :
canonical_model_normalizer.pyutilisefield.get("nullable", False)(défaut NOT NULL).relations.pyutilisefield.get("nullable", True)+requiredinconditionnel (défaut NULL).field.schema.jsondéclarenullable: default truepour les deux contextes.
Problème¶
Sans règle officielle, il est impossible de :
- écrire les tests de non-régression corrects ;
- documenter le comportement de façon cohérente ;
- corriger le runtime sans risque de rupture involontaire.
Décision¶
Forge adopte la règle : champ nullable par défaut, required prioritaire.
Règle officielle¶
| Cas | Résultat officiel |
|---|---|
absent (ni nullable ni required) |
NULL |
nullable: true |
NULL |
nullable: false |
NOT NULL |
required: true (sans nullable) |
NOT NULL |
required: false (sans nullable) |
NULL |
required: true + nullable: true |
NOT NULL (required gagne) |
required: true + nullable: false |
NOT NULL |
Formule unifiée :
nullable = field.get("nullable", True) # défaut True (NULL)
if field.get("required") is True:
nullable = False # required gagne toujours
Exemples¶
→NULL (absent = nullable par défaut)
→ NOT NULL (required force)
→ NULL (explicite)
→ NOT NULL (required prioritaire sur nullable)
Justification¶
- Cohérence avec
field.schema.json: le schéma déclarenullable: default true. La règle retenue respecte ce défaut. - Cohérence entre entité et pivot : les deux contextes partagent
field.schema.json. La règle doit être identique. requiredcomme signal SQL :required: truesignifie "obligatoire" — ce qui impliqueNOT NULL. Le laisser contredire parnullable: trueest contre-intuitif.- Optionnel par défaut : un champ non marqué
requiredounullable: falseest naturellement optionnel (NULL).
Pourquoi rejeter les autres options¶
Option A (conserver le comportement actuel) : entérine trois incohérences sans règle. Les correctifs futurs seraient imprévisibles. Rejetée.
Option C (NOT NULL par défaut partout) : contredit field.schema.json (default: true).
Imposerait de modifier le schéma. Rejetée.
Option D partielle (nullable gagne sur required) : laisser nullable: true écraser
required: true est surprenant — un champ obligatoire en formulaire peut accepter NULL
en base. Rejetée.
Conséquences¶
Sur le runtime (non corrigé dans ce ticket)¶
Le code actuel dévie de cette règle dans canonical_model_normalizer.py :
# Actuel — déviant
nullable = bool(field.get("nullable", False)) # défaut NOT NULL — incorrect
if field.get("required") and not field.get("nullable"):
nullable = False # required ne gagne pas si nullable=True
La correction sera apportée dans NULLABLE-CONTRACT-003.
Sur la documentation (non corrigée dans ce ticket)¶
entity-schema.mdditnullable: défaut true— c'est correct selon la règle retenue, mais le comportement runtime actuel estFalse. À corriger dans NULLABLE-DOC-FIX-001 pour préciser l'écart temporaire.pivots-many-to-many.mdne précise pas la priorité derequired. À corriger dans NULLABLE-DOC-FIX-001.
Sur les tests (non modifiés dans ce ticket)¶
test_build_model_canonical_normalizer.py::TestConstraints::test_default_nullable_is_false
teste le comportement actuel (déviant). Ce test devra être mis à jour dans
NULLABLE-CONTRACT-003 pour refléter la règle officielle (absent → NULL).
Sur les starters et fixtures¶
Les starters sont déjà en format canonique avec nullable explicite sur les champs
concernés. Aucun impact attendu sur les starters existants lors de la correction runtime.
Hors périmètre de ce ticket¶
- Modifier
canonical_model_normalizer.py— voirNULLABLE-CONTRACT-003. - Modifier
relations.py(comportement pivot déjà conforme à la règle retenue). - Modifier les schémas JSON.
- Modifier les fixtures de tests.
- Corriger la documentation utilisateur en détail.
Tickets futurs¶
| Ticket | Objectif | Priorité |
|---|---|---|
NULLABLE-DOC-FIX-001 |
Corriger entity-schema.md, pivots-many-to-many.md, types-forge-mariadb.md |
Haute |
NULLABLE-CONTRACT-003 |
Appliquer la règle officielle dans canonical_model_normalizer.py + tests |
Haute |
Référence¶
- Audit :
docs/history/audits/nullable-contract-audit-001.md - Décision héritière de NULLABLE-CONTRACT-001 (audit) et NULLABLE-CONTRACT-002 (décision)