JSON-LD par la pratique

auteur

Pierre-Antoine Champin (http://champin.net/)

licence

CC-BY-SA

Introduction

JSON-LD (JSON for Linked Data) est une syntaxe concrète pour RDF, basée sur le très populaire format JSON. Mais c’est en même temps plus que cela. JSON-LD permet d’exprimer un graphe RDF en JSON sous une multitude de formes. Et inversement, il permet d’interpréter une large classe de documents JSON comme des graphes RDF.

_images/jsonld-algorithms.svg

Fig. 1 Formes documentaires et algorithmes JSON-LD

JSON-LD est décrit par plusieurs recommandations du W3C:

Note

Ces recommandations décrivent en réalité d’autres formes et d’autres algorithmes, mais qui jouent un rôle moins importants pour les utilisateurs de JSON-LD. Nous ne les aborderons donc pas ici. De plus, il existe une troisième recommandation, JSON-LD 1.1 framing, que nous n’aborderons pas non plus.

L’objectif de ce document est d’initier le lecteur aux bases de JSON-LD.

La forme étendue

Considérons le graphe RDF représenté ci-dessous en Turtle dans le Code source 1, et illustré par la Fig. 2.

Code source 1 Alice et Bob (Turtle)
prefix s: <http://schema.org/>
prefix xsd: <http://www.w3.org/2001/XMLSchema#>

<#alice> a s:Person;
   s:name "Alice";
   s:birthDate "1987-06-05"^^xsd:date;
   s:knows [
      a s:Person;
      s:name "Bob"@en, "ぼぶ"@ja;
      s:knows <#alice>
   ].

digraph { { rank=source Person [label="s:Person"] } { rank=same alice [label="#alice"] bob [label=""] } { rank=same node [shape=box] salice [label="Alice"] sboben [label="Bob"] sbobja [label="ぼぶ"] date [label="1987-06-05"] } alice -> Person [label="a"] alice -> salice [label="s:name"] alice -> date [label="s:birthDate"] alice -> bob [label="s:knows"] bob -> Person [label="a"] bob -> sboben [label="s:name"] bob -> sbobja [label="s:name"] bob -> alice [label="s:knows"] }

Fig. 2 Alice et Bob

Une représentation JSON-LD en forme étendue de ce graphe est donnée dans le Code source 2 ci-dessous. Pour cet exemple et dans tous les suivants, le lien «PLAYGROUND» permet de l’ouvrir dans le JSON-LD playground, qui vous permet de tester interactivement les différents algorithmes de JSON-LD.

Code source 2 Alice et Bob (forme étendue) table
[
   {
      "@id": "http://example.com/#alice",
      "@type": [
         "http://schema.org/Person"
      ],
      "http://schema.org/name": [
         {
            "@value": "Alice"
         }
      ],
      "http://schema.org/birthDate": [
         {
            "@value": "1987-06-05",
            "@type": "http://www.w3.org/2001/XMLSchema#date"
         }
      ],
      "http://schema.org/knows": [
         {
            "@id": "_:b"
         }
      ]
   },
   {
      "@id": "_:b",
      "@type": [
         "http://schema.org/Person"
      ],
      "http://schema.org/name": [
         {
            "@value": "Bob",
            "@language": "en"
         },
         {
            "@value": "ぼぶ",
            "@language": "ja"
         }
      ],
      "http://schema.org/knows": [
         {
            "@id": "http://example.com/#alice"
         }
      ]
   }
]

Cet exemple illustre les différents principes de la forme étendue :

  • Le document est un tableau JSON.

  • Chaque élément de ce tableau représente un nœud du graph. C’est un objet JSON muni d’un attribut @id qui contient un IRI (1er élément dans l’exemple) ou un identifiant local commençant par _: dans le cas d’un nœud vide (blank node, 2ème élément dans l’exemple).

  • Les autres attributs représentent les arcs sortants du nœud; le nom de l’attribut est l’IRI du prédicat, et la valeur est un tableau d’objets JSON, représentant le(s) nœud(s) destination(s) (exemple de valeurs multiples : l’attribut …/name du 2ème élément).

  • Lorsque le nœud destination d’un arc est un IRI ou un nœud vide, il est représenté par un objet JSON muni d’un attribut @id, contenant l’IRI ou l’identifiant local correspondant (cf. les arcs …/knows dans l’exemple).

  • Lorsque le nœud destination d’un arc est un littéral, il contient un attribut @value contenant la valeur lexicale du littéral, et l’un ou l’autre des attributs suivants :

    • @language, qui contient le code de la langue au format BCP 47, dans le cas d’un chaîne linguistique (language string, cf. les deux noms de Bob),

    • @type, qui contient l’IRI du type de donnée, dans les autres cas (cf. la date de naissance d’Alice);

    • dans le cas particulier des littéraux de type xsd:string, @type peut être omis (cf. le nom d’Alice).

  • Cas particulier: la propriété rdf:type est représentée par l’attribut @type, dont la ou les valeurs sont représentées par un simple tableau d’IRIs.

Avertissement

Comme on le voit: l’attribut @type joue deux rôle différents en JSON-LD en fonction du contexte dans lequel il apparaît. Utilisé conjointement à l’attribut @value, il représente le type de donnée d’un littéral. Dans tous les autres cas, il représente un arc rdf:type.

Exercice

  • Ouvrez le Code source 2 dans le playground. Pour chacune des questions ci-dessous, vérifiez dans l’onglet Table ou N-Quads que les triplets ont bien été ajoutés ou modifiés.

  • Modifiez le JSON pour ajouter une date de naissance à Bob.

  • Modifiez le JSON pour préciser que le nom d’Alice est en anglais (en).

  • Ajoutez à Alice le type http://xmlns.com/foaf/0.1/Agent.

  • Supprimez le nom japonnais de Bob.

Il est également possible d’imbriquer hiérarchiquement les descriptions des nœuds :

Exercice

  • En repartant du Code source 2 dans le playground, déplacez le 2ème élément du tableau global (celui qui représente Bob avec tous ses attributs) comme valeur dans la propriété …/knows du 1er élément (qui représente Alice), en remplacement de l’objet JSON { "@id": "_:b" }.

  • Constatez que les triplets générés restent les mêmes.

Pour la suite, nous partirons de la forme étendue imbriquée produite par l’exercice ci-dessus, et qui est rappelée dans le Code source 3 ci-dessous.

Code source 3 Alice et Bob (forme étendue imbriquée) table
[
   {
      "@id": "http://example.com/#alice",
      "@type": [
         "http://schema.org/Person"
      ],
      "http://schema.org/name": [
         {
            "@value": "Alice"
         }
      ],
      "http://schema.org/birthDate": [
         {
            "@value": "1987-06-05",
            "@type": "http://www.w3.org/2001/XMLSchema#date"
         }
      ],
      "http://schema.org/knows": [
         {
            "@id": "_:b",
            "@type": [
               "http://schema.org/Person"
            ],
            "http://schema.org/name": [
               {
                  "@value": "Bob",
                  "@language": "en"
               },
               {
                  "@value": "ぼぶ",
                  "@language": "ja"
               }
            ],
            "http://schema.org/knows": [
               {
                  "@id": "http://example.com/#alice"
               }
            ]
         }
      ]
   }
]

La forme compacte

La forme étendue que nous venons de voir reflète fidèlement la structure du graphe. Elle permet aux algorithmes toRdf et fromRdf (cf. Fig. 1) de passer facilement entre JSON-LD et les autres syntaxes RDF. Mais le document JSON résultant est très verbeux, et très peu idiomatique. En d’autres termes, il ne ressemble pas du tout aux documents JSON habituellement manipulés par les développeurs.

L’algorithme compact permet de transformer ce document sous forme étendue dans une forme moins verbeuse et plus idiomatique. Il utilise pour cela un objet JSON particulier appelé contexte, qui permet de paramétrer la transformation effectuée par l’algorithme compact.

Une stratégie habituelle en RDF pour réduire la verbosité (que l’on peut voir à l’œuvre dans le Code source 1) est de définir des préfixes, qui serviront ensuite à abréger les IRIs (exemple : s:name au lieu de http://schema.org/name), ou à utiliser des IRIs relatifs (exemple : #alice). Voici un exemple de contexte JSON-LD définissant l’IRI de base, et un préfixe pour les IRIs du vocabulaire Schema.org.

Code source 4 Contexte simple compacted code-expanded-ex1b
{
   "@context": {
     "@base": "http://example.com/",
     "s": "http://schema.org/"
   }
}

Le résultat de la compaction du Code source 3 avec le contexte ci-dessus, que vous pouvez vérifier en cliquant sur le lien playground du contexte, donne le résultat suivant :

Code source 5 Forme compacte avec des préfixes table
{
  "@context": {
    "@base": "http://example.com/",
    "s": "http://schema.org/"
  },
  "@id": "#alice",
  "@type": "s:Person",
  "s:birthDate": {
    "@type": "http://www.w3.org/2001/XMLSchema#date",
    "@value": "1987-06-05"
  },
  "s:knows": {
    "@id": "_:b",
    "@type": "s:Person",
    "s:knows": {
      "@id": "#alice"
    },
    "s:name": [
      {
        "@language": "en",
        "@value": "Bob"
      },
      {
        "@language": "ja",
        "@value": "ぼぶ"
      }
    ]
  },
  "s:name": "Alice"
}

On constate que :

  • Le contexte a été intégré au document (afin que ce dernier demeure autosuffisant).

  • Chaque tableau ne contenant qu’un seul élément a été remplacé par cet élément (exemples : attributs @type, s:knows).

  • Chaque objets JSON contenant uniquement un attribut @value a été remplacé par la valeur de cet attribut (exemple : le nom d’Alice).

  • Les IRIs correspondant aux préfixes déclarés ont été abrégés en utilisant ces préfixes (en l’occurrence, ceux commençant par http://schema.org/).

  • Les autres IRIs ont été laissés tels quels (exemple : le type de la date de naissance d’Alice).

  • Les IRIs valeurs de l’attribut @id ont été remplacés par des IRIs relatifs à l’IRI de base (quand c’était possible).

Exercice

  • Ajoutez un préfixe xsd pour que le type de la date de naissance soit également abrégé.

  • Remplacez l’IRI de base par http://example.com/toto/. Que constatez vous sur les attributs @id?

  • Remplacez l’IRI de base par http://autre.example.com/. Que constatez vous sur les attributs @id?

Ajout d’alias

La forme compacte du Code source 5 est déjà bien moins verbeuse que la forme étendue, mais elle n’est pas encore très idiomatique. En particulier, on évite en général d’avoir des caractères spéciaux dans les noms des attributs, car ceux-ci empêchent d’accéder aux attributs avec la notation objet.attribut (en Javascript, notamment).

En plus des préfixes, le contexte JSON-LD nous permet de définir des alias, qui remplaceront complètement l’IRI correspondant. Ceci est illustré par le Code source 6 ci-dessous.

Code source 6 Contexte définissant des alias compacted code-expanded-ex1b
{
   "@context": {
     "@base": "http://example.com/",
     "s": "http://schema.org/",
     "xsd": "http://www.w3.org/2001/XMLSchema#",

     "name": "http://schema.org/name",
     "knows": "http://schema.org/knows"
   }
}

Ouvrez le playground et constatez que l’attribut s:name et devenu name, et que s:knows est devenu knows.

Indication

Il n’y a pas de distinction entre les préfixes et les alias. En fait, l’algorithme compact essaye de raccourcir au maximum les IRIs; s’il trouve dans le contexte une entrée qui correspond exactement à l’IRI, il remplace directement par cette entrée (alias); s’il ne trouve qu’une entrée correspondant au début de l’IRI, il remplace par une abréviation prefixe:suffixe.

Exercice

  • Ouvrez le playground pour le Code source 6.

  • Dans le contexte, remplacez "name": "http://schema.org/name" par "name": "s:name", et constatez que le résultat reste inchangé.

  • Ajoutez un alias born pour l’IRI http://schema.org/birthDate, et constatez que cet attribut est bien remplacé par l’alias.

  • Ajoutez au contexte une entrée "ident": "@id". Constatez que les attributs @id ont bien été remplacés.

  • Ajoutez au contexte un alias Person pour l’IRI http://schema.org/Person. Que constatez-vous ?

  • Ajoutez les alias manquants pour qu’aucun attribut dans le document (à part @context et son contenu) ne contienne de caractères spéciaux (@, :…).

Dans l’exercice précédent, on a vu que:

  • on peut utiliser les préfixes à l’intérieur même du contexte ;

  • les alias ne doivent pas forcément correspondre à la fin de l’IRI ou au mot-clé ;

  • on peut définir des alias même sur les mots-clés de JSON-LD (commençant par @), à part @context ;

  • les alias s’appliquent aux propriétés, mais également aux valeurs de @type.

Ceci nous donne le contexte suivant :

Code source 7 Contexte définissant des alias compacted code-expanded-ex1b
{
   "@context": {
     "@base": "http://example.com/",
     "s": "http://schema.org/",
     "xsd": "http://www.w3.org/2001/XMLSchema#",

     "name": "s:name",
     "born": "s:birthDate",
     "knows": "s:knows",
     "Person": "s:Person",
     "date": "xsd:date",
     "ident": "@id",
     "type": "@type",
     "lang": "@language",
     "val": "@value"
   }
}

Coercion de types

Le contexte ci-dessus produit désormais un objet JSON avec la forme suivante (si on « escamote » le contexte).

Code source 8 Contexte définissant des alias
{
  "@context": {},
  "ident": "#alice",
  "type": "Person",
  "born": {
    "type": "date",
    "val": "1987-06-05"
  },
  "knows": {
    "ident": "_:b",
    "type": "Person",
    "knows": {
      "ident": "#alice"
    },
    "name": [
      {
        "lang": "en",
        "val": "Bob"
      },
      {
        "lang": "ja",
        "val": "ぼぶ"
      }
    ]
  },
  "name": "Alice"
}

C’est déjà plus idiomatique que le Code source 5, mais ça n’est pas encore parfait. Il semble par exemple superflu de spécifier le type de données de la valeur de born, qui sera a priori toujours une date. On peut spécifier ceci dans le contexte en remplaçant la définition de born (actuellement une simple chaîne contenant l’IRI) par un object JSON décrivant à la fois l’IRI et le type de données attendu :

Code source 9 définition détaillée du terme born dans le contexte:
{
  "@id": "s:birthDate",
  "@type": "xsd:date"
}

Il faut comprendre que @id indique l’IRI correspondant à l’attribut ainsi défini, et que @type s’applique aux valeurs que prend cet attribut.

Exercice

  • Dans le contexte du Code source 7, remplacez la définition de born par la définition détaillée ci-dessous.

  • Constatez que la valeur de born dans la forme compacte est maintenant une simple chaîne de caractère. Le type de données est maintenant implicitement porté par le contexte.

  • Gardez ce contexte modifié à portée de main, vous l’utiliserez dans l’exercice suivant.

De la même manière, knows est toujours un lien vers une autre ressource. Lorsque cet objet est décrit en détail (comme c’est le cas pour l’attribut knows d’Alice), il est normal que la valeur de knows soit un objet JSON, mais lorsqu’il est simplement référencé (comme c’est le cas pour l’attribut knows de Bob, qui « pointe » simplement vers Alice), on pourrait se contenter d’une simple chaîne de caractère contenant son identifiant.

Ceci est indiqué de manière similaire au cas précédent, en indiquant @id comme type de valeur.

Code source 10 définition détaillée du terme knows dans le contexte:
{
  "@id": "s:knows",
  "@type": "@id"
}

Exercice

  • Dans le contexte de l’exercice précédent, remplacez la définition de knows par la définition détaillée ci-dessous. Constatez que la valeur de knows dans la forme compacte est remplacée, quand c’est possible, par une simple chaîne de caractères.

  • Gardez ce contexte modifié à portée de main, vous l’utiliserez dans l’exercice suivant.

Coercion de langues et alias multiples

La représentation des noms de Bob dans plusieurs langues n’est pas non plus très idiomatique. Il serait préférable, par exemple, d’avoir plusieurs attributs JSON différents, portant à la fois l’information du prédicat RDF et l’information de la langue de la valeur – selon un processus similaire à la coercion de type vue juste avant.

On peut obtenir ce résultat en ajoutant au contexte les deux définitions suivantes :

Code source 11 un exemple de contexte avec coercion de langue
{
  "@context": {
    "name_en": {
      "@id": "http://schema.org/name",
      "@language": "en"
    },
    "name_ja": {
      "@id": "http://schema.org/name",
      "@language": "ja"
    }
  }
}

Exercice

  • Dans le contexte de l’exercice précédent, incluez les définitions de name_en et name_ja comme dans le Code source 11.

  • Constatez que l’attribut name de Bob a été remplacé par deux attributs name_en et name_ja dont les valeurs sont de simples chaînes.

En plus de la coercion de langue, l’exercice précédent démontre une propriété importante de JSON-LD: le même IRI peut avoir plusieurs alias. L’algorithme compact choisit le plus approprié, c’est à dire celui qui produira la forme la moins verbeuse.

Astuce

Une autre manière idiomatique, en JSON, pour représenter une même valeur dans plusieurs langues, consiste à utiliser un objet dont les clés sont les codes de langues, comme dans l’exemple ci-dessous. Il est possible de paramétrer le contexte JSON-LD pour obtenir ce type de représentation, mais cela dépasse le cadre de ce tutoriel.

{
  "ident": "_:b",
  "name": {
    "en": "Bob",
    "ja": "ぼぶ"
  }
}

À titre indicatif, le Code source 12 contient la forme compacte (incluant le contexte) à laquelle vous devriez avoir abouti à l’issu des exercices précédents. On a clairement séparé le contexte (qui peut être ignoré par les développeurs) du reste de l’objet JSON. Ceci met bien en évidence la différence entre la forme compacte, succinte et idiomatique, et la forme étendue du Code source 2. Le lien « PLAYGROUND » permet également de vérifier que l’algorithme expand permet de reconstruire, à partir de cette forme compacte munie de son contexte, la forme étendue initiale.

Code source 12 corrigé des exercices précédents expanded
{
  "@context": {
    "@base": "http://example.com/",
    "s": "http://schema.org/",
    "xsd": "http://www.w3.org/2001/XMLSchema#",
    "name": "s:name",
    "name_en": {
      "@id": "name",
      "@language": "en"
    },
    "name_ja": {
      "@id": "name",
      "@language": "ja"
    },
    "born": {
      "@id": "s:birthDate",
      "@type": "xsd:date"
    },
    "knows": {
      "@id": "s:knows",
      "@type": "@id"
    },
    "Person": "s:Person",
    "date": "xsd:date",
    "ident": "@id",
    "type": "@type",
    "lang": "@language",
    "val": "@value"
  },


  "ident": "#alice",
  "type": "Person",
  "born": "1987-06-05",
  "knows": {
    "ident": "_:b",
    "type": "Person",
    "knows": "#alice",
    "name_en": "Bob",
    "name_ja": "ぼぶ"
  },
  "name": "Alice"
}

Convertir du JSON natif en RDF

Dans les sections précédentes, nous sommes partis d’un graphe RDF, et nous avons montré comment JSON-LD permettait d’exprimer ce graphe sous forme d’un document JSON compact et idiomatique, à partir duquel le graphe RDF peut être reconstruit.

Ceci permet d’envisager un autre usage de JSON-LD : ré-interpréter comme des graphes RDF des documents JSON existants, non conçus a priori pour représenter du RDF. Il suffit pour cela de leur ajouter un contexte appoprié, et de leur appliquer l’algorithme expand puis toRdf.

Indication

En fait, on peut appliquer toRdf directement, car de dernier accepte également des documents sous forme compacte; dans ce cas, il applique préalablement expand.

Il existe 3 manières d’ajouter un contexte à un document JSON quelconque :

  • ajouter un attribut @context contenant l’objet JSON décrivant le contexte (comme vu précédemment, par exemple dans le Code source 12) ;

  • ajouter un attribut @context contenant l’URL d’un document JSON décrivant le contexte, comme illustré ci-dessous dans le Code source 13) ;

  • sans modifier le document JSON, mais en fournissant le contexte séparément à l’algorithme expand ou toRdf (cette option, nommée expandContext, n’est pas disponible dans le playground, mais est offerte par les bibliothèques implémentant l’API JSON-LD).

Code source 13 JSON-LD sous forme compacte, avec un contexte distant table
{
  "@context": "http://champin.net/2020/json-ld-tuto/_static/context-ex1.jsonld",
  "ident": "#alice",
  "type": "Person",
  "name": "Alice",
  "born": "1987-06-05",
  "knows": {
    "ident": "_:b",
    "type": "Person",
    "name_en": "Bob",
    "name_ja": "ぼぶ",
    "knows": "#alice"
  }
}

Inverser le sens d’un arc

Dans certaines circonstances, la direction des attributs JSON est inversée par rapport à la direction des arcs que l’on souhaite produite dans le graphe RDF. Considérons par exemple le JSON du Code source 14 ci-dessous.

Code source 14 format JSON standard pour des données généalogiques
{
  "name": "Alice",
  "parents": [
     {
       "name": "Bob"
     },
     {
       "name": "Charlie"
     }
  ],
  "children": [
     {
       "name": "Dan"
     },
     {
       "name": "Emily"
     }
  ]
}

Nous souhaitons extraire de ce document JSON un graph RDF utilisant le vocabulaire http://schema.org/. Pour cela, on propose le contexte du Code source 15, qui produit le graphe illustré Fig. 3.

Code source 15 un premier contexte pour les données généalogiques table code-json-ex2
{
  "@context": {
    "s": "http://schema.org/",
    "name": "s:name",
    "parents": "s:parent"
  }
}

digraph { a [label=""] b [label=""] Alice [shape=box] c [label=""] Bob [shape=box] Charlie [shape=box] a -> b [label="s:parent"] a -> c [label="s:parent"] a -> Alice [label="s:name"] b -> Bob [label="s:name"] c -> Charlie [label="s:name"] }

Fig. 3 Graph généalogique partiel

On peut noter deux choses sur cet exemple :

  • un objet JSON sans attribut @id produit un nœud vide en RDF ;

  • un attribut JSON n’ayant pas de correspondance dans le contexte est ignoré par l’algorithme expand, et n’a donc pas de contrepartie dans le graphe.

En ce qui concerne l’attribut children, nous souhaiterions l’intégrer également au graphe, mais Schema.org n’a pas de prédicat child, le prédicat parent est censé être suffisant. Le problème est que nous devons donc produire des arcs RDF qui sont en sens inverse par rapport aux attributs JSON. Ceci est possible grâce au mot-clé @reverse, dont la valeur est l’IRI du prédicat inverse correspondant à l’attribut.

Ainsi, nous pouvons créer un graphe RDF complet représentant notre graphe généalogique, à l’aide du contexte donné dans le Code source 16. Le graphe résultant est illustré Fig. 4.

Code source 16 un contexte plus complet pour les données généalogiques table code-json-ex2
{
  "@context": {
    "s": "http://schema.org/",
    "name": "s:name",
    "parents": "s:parent",
    "children": {
      "@reverse": "s:parent"
    }
  }
}

digraph { a [label=""] b [label=""] Alice [shape=box] c [label=""] d [label=""] e [label=""] Bob [shape=box] Charlie [shape=box] Dan [shape=box] Emily [shape=box] d -> a [label="s:parent"] e -> a [label="s:parent"] a -> b [label="s:parent"] a -> c [label="s:parent"] a -> Alice [label="s:name"] b -> Bob [label="s:name"] c -> Charlie [label="s:name"] d -> Dan [label="s:name"] e -> Emily [label="s:name"] }

Fig. 4 Graphe Arbre généalogique partiel