Sérialiser datetime et Decimal en JSON avec un contrat Python explicite
Utilisez des chaînes de timestamp ISO et des chaînes décimales, puis reconstruisez les champs explicitement. Un exemple complet de la bibliothèque standard montre les vérifications de fuseau horaire, la précision et les échecs prévisibles.
Dans ce guide
La réponse courte
Utilisez une fonction json.dumps par défaut personnalisée pour encoder les datetimes conscients du fuseau horaire comme des chaînes ISO et les valeurs décimales finies comme des chaînes décimales. Restaurez les champs nommés explicitement après json.loads. Cela préserve la représentation du décalage de timestamp d'échantillon et de Decimal(« 12.30 ») sans traiter les chaînes arbitraires comme des objets typés. L'exemple valide les deux champs, pas un schéma d'entreprise complet.
Définir le contrat d'échange
JSON n'a pas de type datetime ou Decimal dédié. Définissez comment chaque type Python supplémentaire franchira la frontière avant de choisir un encodeur. Ce guide utilise un contrat spécifique : le timestamp est une chaîne ISO contenant un décalage UTC utilisable, et le montant est une chaîne décimale finie. Un consommateur reconstruit les deux champs explicitement. Ni une chaîne ressemblant à une date ni une chaîne ressemblant à un nombre n'est automatiquement un objet Python typé.
L'exemple utilise la bibliothèque standard Python et cible Python 3.11 ou version ultérieure. Son entrée construite est un datetime UTC pour le 29 septembre 2026, à 12h00 et Decimal(« 12.30 »). Le but est de préserver cette représentation, pas de sérialiser des objets arbitraires ou de définir un schéma de paiement complet.
Sérialiser les dates avec fuseau horaire
Un datetime est conscient uniquement lorsque tzinfo n'est pas None et que utcoffset() retourne également une valeur. L'encodeur vérifie les deux conditions avant d'appeler isoformat(). Le rejet des valeurs naïves évite d'assigner silencieusement le fuseau horaire local de la machine ou de confondre une lecture d'horloge murale locale avec un instant spécifique.
Le timestamp UTC d'échantillon devient 2026-09-29T12:00:00+00:00. Une chaîne de décalage identifie le décalage à ce moment-là, mais ne préserve pas un nom de zone IANA ni ses règles futures de changement d'heure. Normalisez en UTC uniquement lorsque cela correspond à votre contrat ; cet exemple ne réinterprète pas un timestamp naïf comme UTC.
Préserver Decimal comme une chaîne
Decimal(« 12.30 ») conserve ses chiffres décimaux et son zéro final. Le convertir en float binaire avant la sérialisation change la représentation et peut introduire un arrondi. L'encodeur retourne plutôt str(obj), faisant du montant une chaîne JSON. Le destinataire peut le reconstruire avec Decimal sans d'abord le faire passer par un float.
L'exemple rejette les valeurs Decimal non finies. Une chaîne décimale ne spécifie toujours pas la monnaie, un montant maximal ou les chiffres fractionnels autorisés. Ce sont des contraintes commerciales distinctes. JSON prend également en charge les valeurs numériques natives ; choisir une chaîne décimale ici est un contrat d'interopérabilité explicite, pas une affirmation que JSON ne peut pas contenir de nombres.
Construire un encodeur explicite
L'exemple complet comprend les imports, l'encodage, la reconstruction et l'entrée d'échantillon. json.dumps appelle default uniquement pour les objets que son encodeur normal ne peut pas gérer. La fonction personnalisée gère datetime et Decimal ; les autres objets personnalisés non pris en charge soulèvent TypeError. Les chaînes, entiers, listes et dictionnaires ordinaires restent de la responsabilité de l'encodeur intégré.
allow_nan=False rejette le float natif NaN et Infinity pendant la sérialisation. La vérification de Decimal finie est séparée car Decimal est géré par la fonction personnalisée. Aucune dépendance tierce n'est nécessaire. L'exemple ci-dessous est illustratif ; sa sortie attendue est expliquée plutôt que présentée comme un test exécuté.
import json
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
def contract_encoder(obj):
if isinstance(obj, datetime):
if obj.tzinfo is None or obj.utcoffset() is None:
raise TypeError("Timezone-aware datetime required")
return obj.isoformat()
if isinstance(obj, Decimal):
if not obj.is_finite():
raise TypeError("Finite Decimal required")
return str(obj)
raise TypeError("Unsupported custom value")
def reconstruct_data(data):
if not isinstance(data, dict):
raise ValueError("Object required")
if not all(isinstance(data.get(key), str) for key in ("timestamp", "amount")):
raise ValueError("timestamp and amount must be strings")
timestamp = datetime.fromisoformat(data["timestamp"])
if timestamp.tzinfo is None or timestamp.utcoffset() is None:
raise ValueError("Timezone-aware timestamp required")
amount = Decimal(data["amount"])
if not amount.is_finite():
raise ValueError("Finite amount required")
return {"timestamp": timestamp, "amount": amount}
data = {"timestamp": datetime(2026, 9, 29, 12, 0, tzinfo=timezone.utc),
"amount": Decimal("12.30")}
encoded = json.dumps(data, default=contract_encoder, allow_nan=False)
print(encoded)
restored = reconstruct_data(json.loads(encoded))
print(restored["timestamp"].isoformat(), str(restored["amount"]))
Lire les champs de retour
json.loads restaure les dictionnaires, les listes et les valeurs de base. Il laisse timestamp et amount comme chaînes. reconstruct_data vérifie que la valeur de niveau supérieur est un objet et que les deux champs requis sont des chaînes, puis appelle datetime.fromisoformat et Decimal explicitement. Il exige également un décalage de timestamp utilisable et un montant fini.
Ne convertissez pas chaque chaîne qui ressemble à une date ou à un nombre. La signification du champ provient du schéma convenu. Ces vérifications couvrent deux champs ; elles ne rejettent pas chaque propriété supplémentaire ni n'appliquent les politiques de taille, de monnaie, d'autorisation et de plage. Les applications doivent ajouter les contraintes que leur frontière nécessite réellement.
Inspecter la sortie illustratrice
Pour l'entrée construite, la première ligne imprimée est {« timestamp »: « 2026-09-29T12:00:00+00:00 », « amount »: « 12.30 »}. La seconde est 2026-09-29T12:00:00+00:00 12.30. Ce sont des lignes attendues illustratives dérivées du code, pas un rapport d'un test exécuté. Le montant est entre guillemets dans JSON et se termine toujours par un zéro après reconstruction.
Comparez les valeurs reconstruites typées avec le contrat prévu. Une chaîne décodée seule ne prouve pas que la reconstruction a eu lieu. Le changement du contrat de sortie en un objet étiqueté ou un montant JSON numérique nécessiterait des modifications correspondantes dans les consommateurs ; ne mélangez pas accidentellement les formats.
Gérer les valeurs invalides de manière prévisible
L'encodage d'un timestamp naïf, d'un Decimal non fini ou d'un autre objet personnalisé non pris en charge soulève l'erreur TypeError explicite dans l'exemple. Un JSON mal formé peut soulever json.JSONDecodeError. Un texte de timestamp ISO mal formé soulève ValueError. Un texte décimal invalide soulève normalement decimal.InvalidOperation sous le contexte décimal par défaut, pas ValueError.
Les vérifications de reconstruction soulèvent ValueError pour un type de niveau supérieur incorrect, des champs manquants ou non chaînes, un timestamp sans décalage et un montant non fini. Les pièges Decimal sont configurables, donc le comportement des exceptions peut dépendre du contexte. Gérez les échecs pertinents à la frontière de l'application sans promettre un mot de traceback universel ou le journalisation des charges utiles sensibles.
Savoir ce que JSON ne peut pas préserver
JSON ne se souvient pas si un tableau provenait d'un tuple ou d'une liste, ou si une chaîne provenait d'un datetime ou d'un Decimal. La reconstruction explicite fait donc partie de ce contrat. Le retour d'une chaîne de default est suffisant ici ; les sous-classes JSONEncoder et JSONDecoder personnalisées sont des alternatives optionnelles, pas des exigences obligatoires.
object_hook est appelé pour les objets JSON décodés, pas pour chaque chaîne primitive. Il peut prendre en charge un format d'objet étiqueté conçu explicitement, mais ce serait un contrat différent des deux champs de chaîne nommés utilisés ici. La préservation de la représentation est utile, mais elle n'établit pas la validité commerciale, les règles de fuseau horaire nommées ou la consommation de ressources sûre.
Points à vérifier
- Utilisez Python 3.11 ou version ultérieure pour cet exemple.
- Vérifiez tzinfo et utcoffset() avant d'accepter un datetime.
- Conservez le montant Decimal comme une chaîne JSON plutôt que de le convertir en float.
- Exigez un objet avec des champs timestamp et amount de chaîne pendant la reconstruction.
- Rejetez les montants non finis et les timestamps sans décalage.
- Distinguons ValueError, json.JSONDecodeError et decimal.InvalidOperation à la frontière d'entrée.
Champ d’application
Ceci est une illustration de la bibliothèque standard construite, pas un test exécuté. Les chaînes de décalage ne préservent pas les règles de fuseau horaire nommées. Les chaînes décimales préservent la représentation, pas la monnaie ou la validité commerciale. L'exemple valide deux champs et la finitude ; les applications ont toujours besoin de politiques de plage, de taille et d'autorisation.