Skip to content

feat(annotations): Pydantic schemas pour properties + metadata_extra JSONB #364

Description

@Yanstart

Contexte

Les colonnes annotations.properties et corrections.metadata_extra
sont des JSONB libres. Aucune validation de schéma. Au fil du temps,
plusieurs représentations de la même donnée vont coexister
({"grade": 2} vs {"tnm": {"grade": "II"}}), rendant les requêtes
et les exports training-ready (N3) instables.

Référence : docs/architecture/ANNOTATION_DATA_MODEL_AUDIT.md — gap G9.

Objectif

Imposer des schémas Pydantic explicites pour les JSONB en fonction de
l'annotation_type, valider à l'écriture (route POST/PATCH).

Approche technique proposée

Modèles Pydantic dédiés

# schemas/annotation_properties.py
class ManualAnnotationProperties(BaseModel):
    grade: Optional[Literal["I", "II", "III", "IV"]] = None
    tnm_t: Optional[str] = None
    tnm_n: Optional[str] = None
    tnm_m: Optional[str] = None
    differentiation: Optional[Literal["well", "moderate", "poor", "undifferentiated"]] = None
    free_form: Optional[dict[str, Any]] = None

class AutoAnnotationProperties(BaseModel):
    model_logits: Optional[list[float]] = None
    attention_score: Optional[float] = Field(None, ge=0, le=1)
    free_form: Optional[dict[str, Any]] = None

class CorrectionMetadata(BaseModel):
    original_geometry_id: Optional[UUID] = None
    correction_duration_ms: Optional[int] = Field(None, ge=0)
    tool_used: Optional[Literal["drag_vertex", "redraw", "delete", "split"]] = None
    free_form: Optional[dict[str, Any]] = None

Validation à l'écriture

Dans routes/annotations.py::create_annotation et update_annotation :

if payload.annotation_type == "manual":
    validated = ManualAnnotationProperties.model_validate(payload.properties or {})
elif payload.annotation_type in ("auto", "auto_confirmed"):
    validated = AutoAnnotationProperties.model_validate(payload.properties or {})
payload.properties = validated.model_dump(exclude_none=True)

Échappatoire free_form

free_form: dict permet de garder l'extensibilité sans rouvrir la porte
au drift. Toute clé non-prévue doit y aller — visible en review.

Acceptance criteria (fonctionnel)

  • Module backend/schemas/annotation_properties.py avec les 3 schémas
  • Validation enforced sur POST /api/v1/annotations et PATCH /api/v1/annotations/{id}
  • Validation enforced sur POST /api/v1/corrections
  • Réponse 422 explicite si payload invalide (avec champ fautif)
  • Endpoint admin GET /api/v1/admin/annotations/properties-stats agrégeant la distribution réelle des clés
  • Documentation docs/architecture/ANNOTATION_PROPERTIES_SCHEMAS.md

Review checklist (conditions de validation pour le reviewer PR)

Code

  • Tests unitaires : 1 par schéma × cas valide + 2 cas invalides (≥ 12 tests)
  • Test d'intégration : POST annotation manual avec champ inconnu non-prévu doit échouer (sauf via free_form)
  • Test d'intégration : PATCH d'une annotation existante avec ancien format dégradé proprement (migration douce)
  • Aucune régression sur le test suite annotations existant

Migration de données

  • Script scripts/analyze_annotation_properties.py qui scan la DB et liste les clés observées + fréquence
  • Stratégie de remédiation documentée pour annotations existantes non-conformes

Sécurité

  • Pas d'injection possible via free_form (validation au minimum sur types primitifs)
  • Taille maximale du JSONB enforced (ex : 8 KB par annotation)

Documentation

  • Swagger UI : exemples payloads valides + invalides
  • backend/schemas/info.md mis à jour
  • docs/architecture/ANNOTATION_DATA_MODEL_AUDIT.md : G9 marqué RESOLVED

Dépendances

  • Indépendante — peut démarrer immédiatement
  • Bloque N4 (bulk import — réutilise les schémas) et N3 (export training-ready)

Hors-scope

  • Migration des annotations historiques vers les nouveaux schémas (DB vide)
  • Génération automatique de JSONSchema depuis Pydantic

Références

  • docs/architecture/ANNOTATION_DATA_MODEL_AUDIT.md §3 (G9)
  • Pydantic v2 model_validate

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions