La validation de contenu va au-delà des codes HTTP. Une API peut retourner 200 OK tout en fournissant des données incorrectes, incomplètes ou mal formatées.
Ces erreurs silencieuses passent inaperçues jusqu'à ce qu'elles cassent les clients. La validation de contenu JSON constitue une couche de monitoring essentielle.
Qu'est-ce que la Validation de Contenu API ?
La validation de contenu désigne la vérification automatisée que les réponses API correspondent aux attentes en termes de structure, types et valeurs.
Types de validation
Plusieurs niveaux de validation sont possibles :
| Type | Description | Exemple |
|---|---|---|
| Schema | Structure et types | Champ email est un string |
| Format | Respect des formats | Email valide, date ISO 8601 |
| Valeurs | Sémantique correcte | Prix positif, statut dans enum |
| Cohérence | Relations entre champs | Date fin > date début |
| Contrat | Respect de la documentation | Champs promis présents |
Validation de schema JSON
Vérifiez la structure avec JSON Schema :
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "email", "created_at"],
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"email": {
"type": "string",
"format": "email"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"created_at": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"enum": ["active", "inactive", "pending"]
}
},
"additionalProperties": false
}
Validation de valeurs
Vérifiez la cohérence sémantique :
def validate_order_response(order):
errors = []
# Prix positif
if order.get('total') is not None and order['total'] < 0:
errors.append("Total cannot be negative")
# Cohérence dates
if order.get('shipped_at') and order.get('created_at'):
if order['shipped_at'] < order['created_at']:
errors.append("shipped_at cannot be before created_at")
# Somme des items = total
if order.get('items'):
items_total = sum(item['price'] * item['quantity'] for item in order['items'])
if abs(items_total - order.get('total', 0)) > 0.01:
errors.append("Items total does not match order total")
return errors
Pourquoi la Validation de Contenu est Essentielle
Erreurs silencieuses
Une API retournant des données incorrectes avec 200 OK ne déclenche pas les alertes classiques :
# Réponse problématique - statut 200 mais données incorrectes
{
"status": 200,
"user": {
"id": null, # Devrait être un entier
"email": "not-valid", # Format email invalide
"balance": -500 # Devrait être >= 0
}
}
Détection des régressions
Un déploiement peut modifier involontairement la structure :
# Avant déploiement
{
"user": {
"full_name": "John Doe",
"email": "john@example.com"
}
}
# Après déploiement (régression !)
{
"user": {
"name": "John Doe", # Renommé !
"email_address": "john@example.com" # Renommé !
}
}
La validation de schema détecte ces régressions immédiatement.
Garantie de qualité
Les consommateurs construisent leur logique sur des hypothèses :
// Code client qui crashe si la structure change
const userName = response.user.full_name.toUpperCase();
// TypeError: Cannot read property 'toUpperCase' of undefined
Conformité à la documentation
Validez que l'API respecte ses spécifications :
# OpenAPI spec
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
# Validation automatique contre l'OpenAPI spec
from openapi_core import validate_response
result = validate_response(
spec=openapi_spec,
request=request,
response=response
)
if result.errors:
alert("Response does not match OpenAPI spec", result.errors)
Comment Implémenter la Validation de Contenu
Définition de schemas
Documentez vos réponses avec JSON Schema ou OpenAPI :
# openapi.yaml
components:
schemas:
User:
type: object
required:
- id
- email
- created_at
properties:
id:
type: integer
description: Unique user identifier
email:
type: string
format: email
name:
type: string
nullable: true
created_at:
type: string
format: date-time
status:
type: string
enum: [active, inactive, pending]
default: pending
Validation en monitoring synthétique
Testez régulièrement avec des checks synthétiques :
import jsonschema
import requests
# Schema attendu
USER_SCHEMA = {
"type": "object",
"required": ["id", "email"],
"properties": {
"id": {"type": "integer"},
"email": {"type": "string", "format": "email"},
"name": {"type": "string"}
}
}
def synthetic_check():
response = requests.get("https://api.example.com/users/1")
# Validation HTTP
if response.status_code != 200:
return {"status": "failed", "error": f"HTTP {response.status_code}"}
# Validation JSON parseable
try:
data = response.json()
except ValueError as e:
return {"status": "failed", "error": "Invalid JSON"}
# Validation schema
try:
jsonschema.validate(data, USER_SCHEMA)
except jsonschema.ValidationError as e:
return {"status": "failed", "error": f"Schema violation: {e.message}"}
return {"status": "passed"}
Validation sur trafic réel
Échantillonnez les réponses en production :
import random
from functools import wraps
SAMPLE_RATE = 0.01 # Valider 1% des réponses
def validate_response_sample(schema):
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
response = await func(*args, **kwargs)
# Échantillonnage
if random.random() < SAMPLE_RATE:
try:
jsonschema.validate(response, schema)
metrics.increment('response_validation.passed')
except jsonschema.ValidationError as e:
metrics.increment('response_validation.failed')
logger.warning('Response validation failed',
extra={'error': e.message, 'path': e.path})
return response
return wrapper
return decorator
@app.get("/users/{user_id}")
@validate_response_sample(USER_SCHEMA)
async def get_user(user_id: int):
return await fetch_user(user_id)
Alerting sur violations
Configurez des alertes appropriées :
groups:
- name: content_validation_alerts
rules:
# Taux d'échec de validation
- alert: HighValidationFailureRate
expr: |
rate(response_validation_failed_total[5m])
/ rate(response_validation_total[5m]) > 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "Validation failure rate > 1%"
# Échec sur endpoint critique
- alert: CriticalEndpointValidationFailure
expr: |
increase(response_validation_failed_total{
endpoint=~"/users.*|/orders.*"
}[5m]) > 0
labels:
severity: high
annotations:
summary: "Validation failure on critical endpoint {{ $labels.endpoint }}"
Reporting agrégé
Visualisez les tendances :
# Grafana dashboard
panels:
- title: "Validation Pass Rate"
type: gauge
query: |
sum(rate(response_validation_passed_total[1h]))
/ sum(rate(response_validation_total[1h]))
- title: "Failures by Endpoint"
type: table
query: |
topk(10, sum by (endpoint) (
increase(response_validation_failed_total[24h])
))
- title: "Failures by Error Type"
type: piechart
query: |
sum by (error_type) (
increase(response_validation_failed_total[24h])
)
- title: "Validation Trend"
type: timeseries
query: |
sum(rate(response_validation_failed_total[1h]))
Bonnes Pratiques de Validation de Contenu
Versionner les schemas
Synchronisez schemas et code API :
api/
├── src/
│ └── routes/
│ └── users.py
├── schemas/
│ └── v1/
│ └── user.schema.json
└── tests/
└── validation/
└── test_user_schema.py
# test_user_schema.py
def test_user_response_matches_schema():
response = client.get("/v1/users/1")
schema = load_schema("v1/user.schema.json")
jsonschema.validate(response.json(), schema)
Évolutions additives
Adoptez une approche rétrocompatible :
# Bon : ajout de champ optionnel (rétrocompatible)
{
"id": 1,
"email": "user@example.com",
"avatar_url": "https://..." # Nouveau champ optionnel
}
# Mauvais : renommage de champ (breaking change)
{
"id": 1,
"email_address": "user@example.com" # Renommé depuis 'email' !
}
Niveaux de strictness
Adaptez la sévérité au contexte :
VALIDATION_LEVELS = {
'critical': {
'checks': ['required_fields', 'types', 'formats'],
'on_failure': 'alert_critical'
},
'standard': {
'checks': ['required_fields', 'types'],
'on_failure': 'alert_warning'
},
'informational': {
'checks': ['all'],
'on_failure': 'log_only'
}
}
def validate_with_level(response, schema, level='standard'):
config = VALIDATION_LEVELS[level]
errors = validate(response, schema, checks=config['checks'])
if errors:
handle_failure(errors, config['on_failure'])
Tests des cas edge
Validez les états limites :
def test_empty_list_response():
"""Une liste vide doit être un array vide, pas null"""
response = client.get("/users?status=nonexistent")
assert response.json() == [] # Pas null !
def test_null_optional_fields():
"""Les champs optionnels null doivent être explicites"""
response = client.get("/users/1")
assert 'middle_name' in response.json() # Présent même si null
assert response.json()['middle_name'] is None
def test_pagination_edge_cases():
"""Page vide en fin de résultats"""
response = client.get("/users?page=9999")
assert response.json()['data'] == []
assert response.json()['has_next'] == False
Documentation des choix
Documentez le rationale :
# validation-rules.yaml
schemas:
User:
validations:
email:
format: email
rationale: "RFC 5322 compliant emails required for notifications"
created_at:
format: date-time
nullable: false
rationale: "All users have a creation date, system-generated"
status:
enum: [active, inactive, pending]
rationale: |
- active: Can use the platform
- inactive: Account disabled
- pending: Email not verified
Conclusion
La validation de contenu API transforme le monitoring superficiel en vérification de qualité approfondie.
Les bénéfices :
- Détection des régressions
- Conformité à la documentation
- Protection des consommateurs
- Qualité garantie
Cette discipline constitue un investissement dans la fiabilité perçue de votre API.