JSON-LD par la pratique
- auteur
Pierre-Antoine Champin (http://champin.net/)
- licence
Table des matières
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.
Fig. 1 Formes documentaires et algorithmes JSON-LD
JSON-LD est décrit par plusieurs recommandations du W3C:
JSON-LD 1.1, a JSON-based serialization for Linked Data décrit la syntaxe de JSON-LD, et définit deux formes pour cette syntaxe: la forme étendue, et la forme compacte (plus de détails ci-dessous).
JSON-LD 1.1 processing algorithms and API décrit les algorithmes permettant de passer d’une forme à l’autre, ainsi que de convertir JSON-LD vers et depuis d’autres formats RDF (représentés sur la Fig. 1).
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.
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>
].
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.
[
{
"@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.
[
{
"@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.
{
"@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 :
{
"@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.
{
"@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’IRIhttp://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’IRIhttp://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 :
{
"@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).
{
"@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 :
{
"@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.
{
"@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 deknows
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 :
{
"@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
etname_ja
comme dans le Code source 11.Constatez que l’attribut
name
de Bob a été remplacé par deux attributsname_en
etname_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.
{
"@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
outoRdf
(cette option, nomméeexpandContext
, n’est pas disponible dans le playground, mais est offerte par les bibliothèques implémentant l’API JSON-LD).
{
"@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.
{
"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.
{
"@context": {
"s": "http://schema.org/",
"name": "s:name",
"parents": "s:parent"
}
}
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.
{
"@context": {
"s": "http://schema.org/",
"name": "s:name",
"parents": "s:parent",
"children": {
"@reverse": "s:parent"
}
}
}
Fig. 4 Graphe Arbre généalogique partiel