Migrer vers une sérialisation type-safe dans Durable Functions for Python

Cet article vous montre comment adopter la sérialisation de charges utiles type-safe (également appelée type-aware) dans une application Durable Functions existante qui utilise le modèle de programmation Python. La sérialisation type-safe valide les charges utiles désérialisées par rapport à un type attendu et vous permet d’opter pour un mode strict renforcé qui élimine un risque de désérialisation de charges utiles non fiables.

Adopter une sérialisation type-safe est une bonne pratique recommandée pour toute application Durable Functions utilisant Python, y compris celles qui ne sont pas sensibles à la sécurité. Cela vous aide à détecter tôt les erreurs d’incompatibilité de type, car le SDK valide chaque payload selon le type attendu par votre code au lieu de reconstruire silencieusement n’importe quel type indiqué par les données stockées. Le mode strict renforce également votre application contre la désérialisation des charges non fiables, ce qui renforce la sécurité de votre code. Le azure-functions SDK présente le mode strict comme une bonne pratique, et cet article vous guide pour l’adopter progressivement, en commençant par des étapes rétrocompatibles.

La fonctionnalité est diffusée en deux packages qui fonctionnent ensemble :

  • azure-functions fournit aux sérialiseurs centralisés (df_dumps / df_loads) une validation de type optionnelle et un support du typage strict.
  • azure-functions-durable achemine toute la sérialisation des données de Durable Functions via ces sérialiseurs et ajoute le paramètre expected_type ainsi que la découverte automatique des types aux API d’orchestration et d’entité.

Pour plus de contexte sur les données persistantes de Durable Functions et la manière dont les types personnalisés sont sérialisés, voir Persistance et sérialisation des données dans Durable Functions.

Modifications apportées

Avant cette fonctionnalité, Durable Functions désérialisait les charges utiles d’objets personnalisés en lisant les __module__ champs et __class__ intégrés dans le JSON stocké et en appelant importlib.import_module() pour localiser la classe. Il n’y avait aucune vérification que la classe dans la charge utile correspondait au type que votre code attendait.

La sérialisation avec sûreté de type ajoute :

  • Un argument expected_type facultatif dans les API d’orchestration et d’entité qui désérialisent une charge.
  • Détection automatique du type qui lit l’annotation du type de retour de votre fonction d’activité décorée v2 et de vos fonctions de sous-orchestrateur, et l’utilise comme expected_type sans modifier le code.
  • Un mode strict, choisi avec la AZURE_FUNCTIONS_DURABLE_STRICT_TYPING variable environnement, qui transforme les incompatibilités de type en erreurs dures et désérialise des objets personnalisés sans appeler importlib.import_module().

Le format de sérialisation est resté inchangé. Les types intégrés continuent de sérialiser en JSON simple, et les objets personnalisés utilisent toujours cette {"__class__", "__module__", "__data__"} convention. Cela signifie que le mode lâche est entièrement rétrocompatible : les historiques existants et les orchestrations en vol continuent de se désérialiser comme auparavant.

Prerequisites

  • Une application Durable Functions existante qui utilise le modèle de programmation Python (v1 ou v2).

  • Les versions minimales de paquets suivantes, qui livrent les sérialiseurs centralisés df_dumps / df_loads :

    Version de Python Version minimale azure-functions
    3.13 et après 2.2.0
    3.10 – 3.12 1.26.0
  • azure-functions-durable 1.6.0 ou plus tard.

Note

Si le package installé azure-functions ne fournit pas df_dumps / df_loads, Durable Functions utilise le pipeline de sérialisation hérité. Le format JSON persistant reste le même, mais l’argument et le expected_type mode strict n’ont aucun effet. Mettre à jour les versions du tableau précédent pour permettre la sérialisation validée par type.

Mode lâche comparé au mode strict

La sérialisation sûre au niveau des types offre deux modes.

Comportement Mode lâche (par défaut) Mode strict
S'inscrire Toujours activé Régler AZURE_FUNCTIONS_DURABLE_STRICT_TYPING sur 1, true, ou yes
Incompatibilité de type Il enregistre un avertissement, puis revient au décodeur hérité Augmentations de salaire TypeError
Décodage d’objets personnalisés Utilise importlib.import_module() (ancien chemin d’accès) Appelle directement expected_type.from_json() ; n'appelle jamais import_module
to_json / from_json Contrat Inchangé Doit être symétrique et produire des données sérialisables nativement en JSON (voir Mise à jour to_json et from_json)
Rétrocompatibilité Oui Non. Nécessite des modifications de code

Le mode lâche est sûr à adopter immédiatement car il ne change jamais de comportement pour les charges utiles correctement typées. Le mode strict est un changement délibéré, renforcant la sécurité, qui nécessite les étapes de migration qui suivent.

Migrez progressivement

Adopter la sérialisation type-safe par phases. Les étapes 1 et 2 sont rétrocompatibles et sûres à expédier seules. Terminez les étapes 3 et 4 uniquement lorsque vous êtes prêt à activer le mode strict.

Étape 1 : Mettre à niveau les forfaits

Mettez à jour la configuration requise de votre application vers les versions minimales indiquées dans Prérequis. Par exemple, dans requirements.txt:

azure-functions>=2.2.0
azure-functions-durable>=1.6.0

Après la mise à jour, votre application continue de fonctionner en mode lâche sans aucun changement de comportement. Vous n’avez pas besoin d’apporter d’autres modifications pour que votre application existante fonctionne.

Étape 2 : Adopter la validation de type en mode lâche

En mode lâche, fournir le type attendu afin que le SDK puisse valider les charges utiles désérialisées et enregistrer un avertissement en cas de décalage. Vous pouvez fournir le type de trois façons et les combiner selon les besoins.

Ajouter des annotations de type retour aux activités et aux sous-orchestrateurs. Dans le modèle de programmation Python v2, le SDK découvre automatiquement l’annotation de retour et l’utilise pour valider le résultat. Aucun changement de site d’appel n’est nécessaire.

@myApp.activity_trigger(input_name="city")
def get_weather(city: str) -> WeatherReport:
    return WeatherReport(city=city, temperature_c=21)


@myApp.orchestration_trigger(context_name="context")
def orchestrator(context: df.DurableOrchestrationContext):
    # The WeatherReport return annotation on get_weather is discovered
    # automatically and used to validate the result.
    report = yield context.call_activity("get_weather", "Seattle")
    return report.temperature_c

Passe expected_type explicitement. Un explicite expected_type prime sur une annotation découverte. Utilisez-le lorsque le type de retour n’est pas une classe concrète. Par exemple, des alias génériques comme list[Order] ou Optional[Order] ne peuvent pas être découverts automatiquement.

orders = yield context.call_activity("get_orders", customer_id, expected_type=list)

L’argument expected_type est disponible sur ces API d’orchestration :

  • call_activity et call_activity_with_retry
  • call_sub_orchestrator et call_sub_orchestrator_with_retry
  • call_entity
  • wait_for_external_event
  • get_input

Et sur ces API d’entités, via DurableEntityContext:

  • get_state
  • get_input

Déclarez le type d’entrée d’orchestration sur le déclencheur. Utilisez l’argument input_type sur orchestration_trigger afin que context.get_input() valide l’entrée. Un site d’appel expected_type sur get_input() est prioritaire.

@myApp.orchestration_trigger(context_name="context", input_type=OrderRequest)
def orchestrator(context: df.DurableOrchestrationContext):
    request = context.get_input()  # validated against OrderRequest
    ...

Après cette étape, lancez votre application et consultez les journaux à la recherche d’avertissements d’incompatibilité de type dans le journaliseur azure.functions.DurableFunctions. Éliminez tous les avertissements avant de passer en mode strict. Comme cette étape n’ajoute que des avertissements, elle peut être déployée seule sans risque.

Tip

La découverte automatique de types ne résout que des objets concrets type . Des alias génériques tels que list[Order], dict[str, Order] et Optional[Order] sont résolus en « aucune information de type », et le décodage se rabat sur une résolution au niveau du module uniquement. Fournissez expected_type explicitement quand vous avez besoin de validation pour ces formes.

Étape 3 : Mettre à jour to_json et from_json pour le mode strict

Le mode strict modifie le contrat pour les types personnalisés. En mode strict, to_json() doit retourner une valeur que json.dumps peut sérialiser nativement, par exemple des dictionnaires, des listes, des chaînes, des nombres, des booléens ou None. Vous devez explicitement sérialiser les objets personnalisés imbriqués au lieu de les retourner comme des instances, et from_json() les reconstruire de manière symétrique.

Cette exigence supprime les chaînes __module__ des charges utiles stockées à chaque niveau d’imbrication, de sorte que la désérialisation n’a plus besoin de résoudre les noms de type à partir des données de la charge utile.

class Order:
    def __init__(self, item, hat):
        self.item = item
        self.hat = hat

    @staticmethod
    def to_json(obj):
        return {
            "item": obj.item,
            "hat": Hat.to_json(obj.hat),   # explicit, not obj.hat
        }

    @staticmethod
    def from_json(data):
        return Order(
            item=data["item"],
            hat=Hat.from_json(data["hat"]),  # symmetric
        )

Gérer les charges utiles héritées en vol lors du déploiement. Si votre application peut encore lire des charges utiles écrites en mode lâche avant la mise à niveau, faites tolérer from_json les deux formes. Une valeur imbriquée à encodage lâche arrive comme une instance déjà reconstruite (l’héritage object_hook s’active), tandis qu’une valeur encodée strictement arrive sous forme de dict simple.

    @staticmethod
    def from_json(data):
        hat_data = data["hat"]
        if isinstance(hat_data, Hat):
            hat = hat_data                 # loose-encoded: object already built
        else:
            hat = Hat.from_json(hat_data)  # strict-encoded: plain dict
        return Order(item=data["item"], hat=hat)

Étape 4 : Activer le mode strict

Régler le paramètre de l’application AZURE_FUNCTIONS_DURABLE_STRICT_TYPING à 1, true, ou yes (insensible à la majuscule).

Dans votre fichier local local.settings.json :

{
  "Values": {
    "AZURE_FUNCTIONS_DURABLE_STRICT_TYPING": "true"
  }
}

Ou comme paramètre d’application sur votre application fonctionnelle :

az functionapp config appsettings set --name <APP_NAME> --resource-group <RESOURCE_GROUP> --settings AZURE_FUNCTIONS_DURABLE_STRICT_TYPING=true

En mode strict :

  • Les incompatibilités de type déclenchent TypeError au lieu de consigner un avertissement.
  • Les objets personnalisés sont désérialisés en appelant expected_type.from_json() directement, donc import_module ils ne sont jamais utilisés.
  • Tout site d’appel qui désérialise un objet personnalisé sans un expected_type augmente TypeError. Assurez-vous que chaque site d’appel fournit un type via l’un des mécanismes de l’étape 2 avant d’activer le mode strict.
  • Les entrées de fonctions d’activité ne peuvent pas être des objets personnalisés. Voir la note suivante.

Important

En mode strict, l’entrée d’une fonction d’activité ne peut pas être un objet personnalisé. Lorsque l’hôte invoque une activité, le convertisseur de déclencheur d’activité azure-functions désérialise les données d’entrée sans expected_type, car le processus worker Functions ne transmet pas au convertisseur l’annotation du type de paramètre de l’activité. Une entrée d’objet personnalisé échoue donc avec un ValueError. Passez les entrées d’activité sous forme de valeurs sérialisables nativement JSON, telles que des dictionnaires, listes, chaînes de caractères, nombres, booléens ou None. Si vous devez envoyer un objet personnalisé, convertissez-le avec sa méthode to_json() avant l’appel et reconstruisez-le avec from_json() à l’intérieur de l’activité. Cette limitation ne s’applique qu’aux entrées d’activité. Les valeurs de retour d’activité, les entrées d’orchestration et d’entité, l’état de l’entité et les payloads d’événements externes prennent tous en charge les types personnalisés en mode strict lorsque vous fournissez un type.

Important

Activez le mode strict seulement après que toutes les instances d’application ont été mises à jour et que toute orchestration en vol contenant des historiques lâches encodés a été vidée, ou que vos from_json méthodes tolèrent les deux formes (Étape 3). Une orchestration commencée avant la mise à niveau rejoue son historique d’origine, encodé de manière souple. Si votre code ne peut pas décoder cet historique en mode strict, la relecture échoue.

Implications de versioning pour les orchestrations existantes

La mise à jour vers une sérialisation à typage sûr interrompt les orchestrations en cours lorsque les types de charge utile changent par rapport à l’implémentation héritée. Chaque fois qu’une orchestration continue, elle rejoue son historique stocké. Si un site de décodage attend maintenant un type qui ne correspond pas à ce qu’une ancienne charge utile stockait, le mode strict fait apparaître un TypeError type qui n’était pas présent lors de l’écriture de l’historique, et cette nouvelle erreur casse l’orchestration. Deux changements migratoires courants introduisent ce décalage :

  • Un chemin qui portait auparavant plus d’un type. Si un seul chemin de désérialisation, tel qu’un résultat d’activité, pouvait auparavant retourner différents types d’objets, et que vous l’annotez maintenant avec un seul expected_type, une charge utile stockée utilisant un autre type ne correspond plus et ne décode plus.
  • Types personnalisés utilisés comme entrées d’activité. Comme les entrées d’activité ne peuvent pas être des objets personnalisés en mode strict, l’adoption du mode strict impose de les convertir en valeurs sérialisables au format JSON, ce qui modifie la structure de la charge utile qu’ont conservée les instances en cours d’exécution.

Plus généralement, tout changement qui diffère le type stocké d’une charge utile de celui qu’un site de décodage attend désormais provoque la même erreur. Par exemple, renommer ou déplacer une classe personnalisée après que ses instances ont été enregistrées entraîne le même décalage.

Pour migrer en toute sécurité, utilisez l’une de ces approches :

  • Recommandé : diviser le déploiement avec la version de l’orchestration. Utilisez le versionnage des orchestrations avec la stratégie Strict de correspondance des versions afin que vos nouveaux workers en mode strict ne traitent que les orchestrations démarrées avec la nouvelle version. Cette bonne pratique permet aux deux versions de coexister lors d’une mise à jour progressive et évite les échecs de relecture.
  • Alternative : vidanger d’abord. Laissez toutes les orchestrations en vol se terminer, puis activez le mode strict.

Avant d’activer le mode strict en production, vérifiez que chaque site de décodage d’objets personnalisés fournit un type et que vos classes personnalisées conservent le même nom et le même module qu’elles avaient lors de l’exécution des instances et conservaient leurs charges utiles.

Pour des directives plus larges sur le déploiement sécurisé des modifications affectant les orchestrations en cours, voir Versioning in Durable Functions.

Durcissement de la sécurité

Le mode strict renforce la manière dont les charges utiles sur objets personnalisés sont désérialisées. Au lieu de faire confiance aux noms de modules et de classes intégrés dans une charge utile stockée ou entrante pour localiser un type, le mode strict reconstruit des objets personnalisés en utilisant les expected_type données fournies par votre code, et la sortie en mode to_json() strict ne persiste pas les noms de modules à aucun niveau d’imbriquement. Ce changement supprime la nécessité de résoudre des noms de types arbitraires à partir des données de la charge utile lors de la désérialisation, ce qui constitue une amélioration de la défense en profondeur par rapport au fait de s’appuyer sur les informations de type contenues dans la charge utile.

Si vos charges utiles peuvent contenir des données sensibles, consultez également Travailler avec des données sensibles.