← Expérience

Ingénieur Recherche — Rétro-ingénierie & Évaluation de générateur JSON

CNRS / Laboratoire LIP6Stage de rechercheJuin 2023 — Août 2023Paris

JSON Schema s'est imposé comme standard de description de structures JSON — APIs, pipelines ML, métadonnées. Une question peu étudiée reste ouverte : les générateurs open-source qui produisent automatiquement des instances valides à partir d'un schéma sont-ils fiables ? Ce stage au LIP6, équipe BD, avait pour objet de caractériser les limites de DataGen, l'un de ces générateurs, par rétro-ingénierie manuelle et analyse expérimentale sur corpus réels.

Le problème : génération optimiste et ses limites

Un générateur optimiste parie que la plupart des schémas sont simples — il génère une instance sans vérifier exhaustivement toutes les contraintes logiques, au risque de produire des instances invalides sur les schémas complexes. L'expressivité de JSON Schema rend ce pari risqué : les connecteurs logiques allOf, oneOf, anyOf, not peuvent se composer et s'imbriquer arbitrairement.

Exemple minimal d'un schéma qui piège un générateur optimiste — la propriété prop doit valoir exactement "mandatory", mais le générateur doit déduire cela de la conjonction d'un enum et d'une négation :

{
  "type": "object",
  "properties": {
    "prop": {
      "allOf": [
        { "enum": ["forbidden", "mandatory"] },
        { "not": { "enum": ["forbidden"] } }
      ]
    }
  },
  "required": ["prop"]
}

Rétro-ingénierie du pipeline DataGen

Le code source de DataGen est non documenté (commentaires en portugais) et dépourvu d'architecture explicite. La rétro-ingénierie a été conduite via le débogueur WebStorm, en instrumentant le back-end Docker avec des logs pour suivre l'évolution d'un schéma à chaque étape. Le pipeline interne se décompose en cinq étapes :

Workflow DataGen — pipeline de traitement d'un composant JSON Schema en 5 étapes
  1. Schéma d'entrée — un ou plusieurs schémas JSON, avec désignation du schéma racine pour permettre le référencement croisé.
  2. Schéma étenduschema_extender analyse les mots-clés de composition (allOf, anyOf, oneOf) et fusionne les sous-schémas dans le schéma de base. schema_inverter prend en charge la négation not en produisant un schéma complémentaire — c'est ici que résident la plupart des défaillances observées.
  3. DSL — représentation intermédiaire abstraite, indépendante de JSON/XML, plus facile à manipuler pour la génération.
  4. Dataset — instances synthétiques générées par randomisation à partir du DSL.
  5. Instance finale — sérialisation JSON ou XML via la bibliothèque interne, capable de gérer la récursivité et le regroupement.

Traitement par type

Chaque type primitif est traité par une fonction dédiée. Quelques comportements notables :

  • parseStringType — priorité au champ pattern (génération via RandExp), puis format (date-time, email, uuid…), sinon chaîne aléatoire entre minLength et maxLength. Si minLength est absent, longueur entre 0 et 100.
  • parseNumericType — extrait multipleOf, minimum, maximum, contraintes exclusives. En absence de contraintes, génère aléatoirement entre −1 000 et 1 000. Valeur par défaut de multipleOf : 1 pour les entiers, 0,43 pour les flottants.
  • parseObjectType — génère d'abord les propriétés required, puis les propriétés dépendantes (dependentRequired), les patternProperties (RandExp), et enfin les propriétés additionnelles. Un objet vide est représenté par le sentinel DFJS_EMPTY_OBJECT.
  • parseArrayType — génère les prefixItems, puis les élémentscontains dans les bornes minContains/maxContains, enfin remplit jusqu'à arrLen. Si uniqueItems est actif, jusqu'à 10 tentatives pour garantir l'unicité.

Connecteurs logiques

Tous les connecteurs passent par parseAllSchemaComposition puis parseSchemaComposition :

  • allOf — tous les sous-schémas sont fusionnés via extendSchema.
  • oneOf / anyOf — un sous-schéma est choisi aléatoirement, les autres sont ignorés. C'est une approximation : oneOf exige l'exclusivité, qui n'est pas vérifiée.
  • not — déclenche schema_inverter qui applique des règles d'inversion par type : notNumeric inverse les bornes,notString inverse pattern/format/longueurs, notProperties transforme les propriétés autorisées en propriétés exclues. La composition not imbriquée dans un autre connecteur est la principale source d'instances invalides.

Protocole expérimental

Face à un code source sans documentation et des commentaires en portugais, nous avons adopté un protocole en quatre étapes :

  1. Migration de draft — les datasets couvrent les drafts v4, v6, v7 et 2019-09 ; DataGen n'implémente que la syntaxe 2020-12. Tous les schémas ont été traduits manuellement vers 2020-12.
  2. Génération de masse — exécution de DataGen sur l'ensemble des schémas, collecte simultanée des erreurs de génération.
  3. Validation — chaque instance générée est soumise à json-schema-validator (Java/Jackson), 32× plus rapide que Fge, 5× plus rapide qu'Everit, support complet des drafts v4 à 2020-12.
  4. Classification — les erreurs de validation sont regroupées par classe pour cibler l'analyse du code source.

Une étape annexe a consisté à instrumenter le back-end Docker avec des logs pour suivre l'évolution d'un schéma dans le pipeline — indispensable pour confirmer ou infirmer les hypothèses formulées sur le code.

Contraintes rencontrées

L'environnement Docker de DataGen (5 images, limite de requête à 102 400 octets) a imposé deux contournements : ignorer les schémas dépassant la limite, et fragmenter les datasets en lots avant la génération. Les erreurs 502 disparaissaient en répartissant la charge sur plusieurs requêtes plus petites. Le débogage sous WebStorm a aussi produit des instances vides récurrentes, probablement dues à un conflit de version JavaScript entre le runtime de DataGen et le débogueur.

Résultats sur 6 datasets réels

6 099 instances générées sur 6 datasets couvrant des domaines applicatifs distincts : Snowplow (analytics), WashingtonPost (presse), Kubernetes (infra), GitHub (API), Containment (schémas de test de containment), Handwritten (schémas manuels).

DatasetSchémas satGénérés (sat)Taux générationInstances validesTaux validité
Snowplow42038591,67 %351 / 38591,16 %
WashingtonPost1259374,40 %91 / 9397,85 %
Kubernetes1 08786279,30 %724 / 86284,00 %
GitHub6 3874 45069,29 %3 765 / 4 45084,61 %
Containment4503113,90 %3 / 319,68 %
Handwritten1889551,77 %15 / 9515,79 %

L'écart entre WashingtonPost (97,85 %) et Containment (9,68 %) n'est pas aléatoire : les datasets WashingtonPost et Snowplow contiennent principalement des schémas simples (objects plats, types primitifs). Containment et Handwritten sont construits pour tester les compositions logiques imbriquées — exactement le point faible identifié lors de la rétro-ingénierie.

5 classes d'erreurs identifiées

L'analyse croisée des erreurs de validation et du code source a permis d'isoler cinq classes d'erreurs distinctes, chacune documentée avec un schéma minimal reproductible :

  1. not imbriqué dans allOf/oneOf/anyOf — l'inversion de schéma est appliquée mais le résultat n'est pas reconcilié avec les contraintes du connecteur parent.
  2. oneOf sans vérification d'exclusivité — un sous-schéma est choisi aléatoirement ; si plusieurs sous-schémas sont satisfaisables par la même instance, la contrainte d'exclusivité de oneOf est violée.
  3. Inversion numérique incomplètenotNumeric inverse les bornes mais ne gère pas les cas où l'intervalle interdit est un sous-intervalle d'un intervalle plus large.
  4. Compositions imbriquées multi-niveaux — la récursivité dans parseAllSchemaComposition traite chaque niveau indépendamment sans propager les contraintes des niveaux parents.
  5. Migration de draft incomplète — certains mots-clés renommés entre drafts (ex. itemsprefixItems en 2020-12) produisent un schéma étendu incorrect si la traduction est partielle.

Conclusion

DataGen est performant sur les schémas industriels courants (80–98 % de validité sur Snowplow, WashingtonPost, Kubernetes). Son talon d'Achille est la composition logique imbriquée : dès qu'un not apparaît à l'intérieur d'un autre connecteur, ou que plusieurs connecteurs s'enchaînent sur plus d'un niveau, le taux de validité s'effondre.

Ce résultat confirme l'hypothèse des générateurs optimistes : ils fonctionnent bien tant que les schémas restent dans leur zone de confort. L'approche LLM, explorée en parallèle dans l'équipe, montre des résultats supérieurs (95–98 %) précisément sur ces cas de composition complexe — au prix d'une latence et d'un coût de génération incomparables.

6 099instances générées et analysées
6datasets réels évalués
97,85 %meilleur taux (WashingtonPost)
5classes d'erreurs documentées

Stack

JavaScriptJavaScript
Node.jsNode.js
Java
PythonPython
JSON SchemaJSON Schema
DockerDocker
WebStormWebStorm
IntelliJ IDEAIntelliJ IDEA

Références

  1. JSON Schema — json-schema.org
  2. DataGen — github.com/wurzy/DataGen
  3. json-schema-validator (Java/Jackson) — github.com/networknt/json-schema-validator
  4. Attouche et al., Witness Generation for JSON SchemaarXiv:2202.12849
  5. Attouche et al., A Tool for JSON Schema Witness Generation, EDBT 2021
  6. Baazizi et al., Not Elimination and Witness Generation for JSON Schema, CoRR abs/2104.14828
  7. Datasets : Snowplow, WashingtonPost, Kubernetes, Containment

Équipe

Abdelkader BoumessaoudSorbonne Universitéabdelkader.boumessaoud.pro@gmail.com
Attouche LyesUniversité Paris-Dauphinelyes.attouche@dauphine.fr
Mohamed-Salim AissiSorbonne Universitésaissi1701@gmail.com

Tuteur de stage / Coordinateur

Mohamed-Amine Baazizi

Maître de conférences, LIP6 — Sorbonne Université

mohamed-amine.baazizi@lip6.fr