Dans le précédent article, nous avons vu une première approche pour convertir un JSON en CSV avec Spark. Néanmoins, nous nous étions arrêtés aux structures imbriquées, mais en évitant le cas des array. Cette fois, nous allons nous attaquer à ce type de donnée.

Dans cet article, nous verrons progressivement une approche pour retirer les array des structures de données. Puis nous découvrirons un algorithme plus générique, fonctionnant dans les cas où sont présents plusieurs niveaux d'imbrication entre struct et array.

Partons d'un cas simple

Des structures de données contenant des array sont assez courantes. Par exemple :

{
  "firstname": "John",
  "lastname": "Doe",
  "contact": [
    { "type": "email", "value": "[email protected]" },
    { "type": "twitter", "value": "jdoe87" }
  ]
}

Vu de Spark, le document ci-dessus possède la schéma suivant (en se basant sur une description simplifiée en EDN) :

{:firstname string
 :lastname  string
 :contact   [{:type  string
              :value string}]}

Notre objectif consistera à retirer la partie array de contact pour remonter le struct sous-jacent. Pour cela nous allons utiliser la fonction [explode de Spark](https://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.sql.functions$@explode(e:org.apache.spark.sql.Column):org.apache.spark.sql.Column). Cette fonction permet de partir d'une ligne contenant un array et de la découper en autant de ligne qu'il y a d'élément dans le tableau.

En appliquant explode au champ contact, nous obtenons le schéma suivant :

{:firstname string
 :lastname  string
 :contact   {:type  string
             :value string}}

Et en reconstruisant la structure à la façon de notre précédent article, nous avons le CSV suivant :

firstname, lastname, contact_type, contact_value
John,      Doe,      email,        [email protected]
John,      Doe,      twitter       jdoe87

Imbrication array et structure

Partons maintenant du document suivant et cherchons à le désimbriquer :

{
  "firstname": "John",
  "lastname": "Doe",
  "contact": [
    { "type": "email", "value": [{ "id": "[email protected]" }] },
    { "type": "internal", "value": [
        { "id": "jdoe-dev" },
        { "dept": "ops", "id": "jdoe-ops" }
      ] }
  ]
}

Le document ci-dessus possède du point de vue de Spark le schéma suivant :

{:firstname string
 :lastname  string
 :contact   [{:type  string
              :value [{:dept string
                       :id   string}]}]}

Il y a ici deux difficultés : 1/ nous sommes en présence d'un schéma imbriquant structure et tableau à plusieurs niveaux, 2/ le champ value n'a pas exactement la même structure d'une ligne à l'autre dans contact. Pour le second point, le champ dept n'est en effet présent que dans la dernière ligne.

Pour le premier point, il s'agira de détecter récursivement la présence de array et d'y appliquer le même principe vu précédemment (retirer le array et remonter le struct sous-jacent) en se basant sur explode. Pour le second point, Spark aligne automatiquement le schéma pour l'ensemble du document en recherchant un dénominateur commun. C'est ce que nous voyons dans le schéma précédent, où tous les champs value possède le champ dept (par défaut ce champ vaut null).

Voici le schéma de ce document vu par Spark :