SW-9-CSharp-JSONLD – JSON-LD avec dotNetRDF (twin C#)

Navigation : Index | 8-SHACL | 9-Python | 10-RDFStar

Notebook C# / .NET Interactive sur JSON-LD avec dotNetRDF 3.4.0 : @context, parsing, serialisation, Schema.org, @graph, round-trip.

Objectifs pedagogiques : 1. Comprendre @context : le dictionnaire de traduction entre termes JSON courts et IRIs RDF. 2. Parser du JSON-LD en graphe RDF via JsonLdParser. 3. Serialiser un graphe RDF en JSON-LD via JsonLdWriter (forme etendue). 4. Explorer le vocabulaire Schema.org (Product, Offer, AggregateRating). 5. Regrouper plusieurs ressources via @graph. 6. Valider le round-trip JSON-LD <-> Turtle (isomorphisme).

Pourquoi un twin C# ? Ce notebook est le jumeau .NET de SW-9-Python-JSONLD.ipynb : il invoque le vrai moteur dotNetRDF (VDS.RDF.Parsing.JsonLdParser, VDS.RDF.Writing.JsonLdWriter) — pas de workaround, pas de reimplementation jouet — sur le même standard W3C JSON-LD 1.1 (Prong B, EPIC #4956). Subtilite API qui structure tout le notebook : JSON-LD etant un format de dataset, le parser/writer operent sur un TripleStore, pas un Graph unique (cf. helpers).

Prerequis : SW-3 Graph Operations, SW-4 SPARQL. Pas de SPARQL dans ce notebook. .NET 9.0+ (kernel csharp), dotNetRDF 3.x restaure via la directive nuget.

Setup – chargement de dotNetRDF

dotNetRDF 3.4.0 inclut JsonLdParser (interface IStoreReader) et JsonLdWriter (interface IStoreWriter). Ce sont les deux classes principales pour JSON-LD. La version est epinglee dans la cellule : la serie 3.5.x casse JsonLdWriter.Save sous .NET Interactive (voir le commentaire de la cellule).

Pourquoi dotNetRDF supporte JSON-LD : - Meta-package : 3.x inclut tous les parsers/writers (Turtle, NTriples, RDF/XML, JSON-LD). - Standard W3C : conforme a JSON-LD 1.1 (2020) avec @context, @graph, @id, @type, @value. - Performant : utilise Newtonsoft.Json pour le parsing JSON rapide.

// dotNetRDF 3.x : meta-package incluant JsonLdParser / JsonLdWriter

#r "nuget: dotNetRDF, 3.4.0"

// Pin 3.4.0 (et non 3.5.2) : la serie 3.5.x casse JsonLdWriter.Save sous .NET Interactive

// face au Newtonsoft du hote (MissingMethodException JToken.ToString(Formatting), mesure 2026-09-19) ;

// 3.4.0 execute de bout en bout et n emis plus aucun CS1701.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.IO;

using Newtonsoft.Json;

using Newtonsoft.Json.Linq;

using VDS.RDF;

using VDS.RDF.Parsing;

using VDS.RDF.Writing;

using StringWriter = System.IO.StringWriter;

static void Show(object o) { try { display(o); } catch { Console.WriteLine(o); } }

static void Show(string label, object o) => Show($"{label}: {o}");

Show("dotNetRDF", "3.4.0 charge — JsonLdParser (IStoreReader) / JsonLdWriter (IStoreWriter) disponibles.");
Installed Packages
  • dotNetRDF, 3.4.0
dotNetRDF: 3.4.0 charge — JsonLdParser (IStoreReader) / JsonLdWriter (IStoreWriter) disponibles.

Helpers de conversion RDF <-> JSON-LD

Le notebook definit trois fonctions utilitaires pour les conversions courantes : - LoadJsonLd(string json) : parse un JSON-LD en IGraph. - SerializeToJsonLd(IGraph g) : serialise un IGraph en JSON-LD forme etendue. - SerializeToTurtle(IGraph g) : serialise en Turtle (pour comparaison).

Pourquoi des helpers : - DRY : evite la repetition du code de conversion. - Lisibilite : le notebook se concentre sur les concepts, pas sur les details d’API. - Encapsulation de la subtilite API : JSON-LD se charge dans un TripleStore (dataset), pas un Graph unique comme les autres serialisations — LoadJsonLd parse le store puis en extrait le graphe par defaut, SerializeToJsonLd emballe le graphe dans un store pour le writer. Le reste du notebook manipule des IGraph normaux.

Note de portee : JsonLdWriter produit la forme etendue (chacun des triplets est explicite). Pour la forme compactee (avec @context), il faut post-traiter la sortie.

// --- Helpers : JSON-LD <-> TripleStore <-> Graph + Turtle ---
// Parse JSON-LD -> TripleStore -> on renvoie le graphe par defaut (dataset a un seul graphe)
static IGraph LoadJsonLd(string json)
{
    var store = new TripleStore();
    using (var reader = new StringReader(json))
        new JsonLdParser().Load(store, reader);
    return store.Graphs.Count > 0 ? store.Graphs.First() : new Graph();
}

// Serialise un graphe RDF -> JSON-LD (on l'emballe dans un TripleStore pour le writer)
static string SerializeToJsonLd(IGraph graph)
{
    var store = new TripleStore();
    store.Add(graph, false);
    var sw = new StringWriter();
    new JsonLdWriter().Save(store, sw);
    return sw.ToString();
}

// Serialise un graphe RDF -> Turtle (comparaison)
static string SerializeToTurtle(IGraph graph)
{
    var sw = new StringWriter();
    new CompressingTurtleWriter().Save(graph, sw);
    return sw.ToString();
}

Show("Helpers", "LoadJsonLd / SerializeToJsonLd / SerializeToTurtle definis.");
Helpers: LoadJsonLd / SerializeToJsonLd / SerializeToTurtle definis.

Partie 1 – @context : le dictionnaire de traduction

Le @context est le coeur de JSON-LD : un dictionnaire qui mappe des termes courts vers des IRIs : - "name": "http://schema.org/name" : le terme "name" correspond a la propriete Schema.org name. - "@vocab": "http://schema.org/" : vocabulaire par defaut (pour les termes non specifies). - "@type": "@id" : la valeur doit etre traitee comme un IRI (pas un literal).

Pourquoi @vocab est utile : - Concision : on declare un namespace par defaut au lieu de prefixer chaque terme. - Compatibilite : Schema.org est le vocabulaire dominant, donc @vocab pointe souvent vers lui.

Mots-cles JSON-LD importants : - @id : identifiant unique (IRI) de la ressource. - @type : classe RDF de la ressource. - @value : valeur d’un literal. - @context : definition du vocabulaire. - @graph : plusieurs ressources dans un meme document. - @language : langue d’un literal. - @container : structure (set, list, etc.).

On construit un @context comme un JObject (Newtonsoft.Json), puis on l’utilise dans un document JSON-LD compact — la cellule suivante affiche les deux.

// --- @context : dictionnaire terme -> IRI (Schema.org) ---
var context = new JObject
{
    ["@vocab"] = "http://schema.org/",
    ["name"] = "http://schema.org/name",
    ["email"] = "http://schema.org/email",
    ["url"] = new JObject { ["@id"] = "http://schema.org/url", ["@type"] = "@id" },
    ["Person"] = "http://schema.org/Person",
    ["knows"] = "http://schema.org/knows"
};
Show("@context (brut)", context.ToString(Formatting.Indented));

// Document JSON-LD compact utilisant ce contexte
var person = new JObject
{
    ["@context"] = context,
    ["@id"] = "http://example.org/alice",
    ["@type"] = "Person",
    ["name"] = "Alice Martin",
    ["email"] = "alice@example.org",
    ["url"] = "http://alice.example.org",
    ["knows"] = new JArray { "http://example.org/bob", "http://example.org/carol" }
};
Show("Document JSON-LD compact", person.ToString(Formatting.Indented));
@context (brut): {
  "@vocab": "http://schema.org/",
  "name": "http://schema.org/name",
  "email": "http://schema.org/email",
  "url": {
    "@id": "http://schema.org/url",
    "@type": "@id"
  },
  "Person": "http://schema.org/Person",
  "knows": "http://schema.org/knows"
}
Document JSON-LD compact: {
  "@context": {
    "@vocab": "http://schema.org/",
    "name": "http://schema.org/name",
    "email": "http://schema.org/email",
    "url": {
      "@id": "http://schema.org/url",
      "@type": "@id"
    },
    "Person": "http://schema.org/Person",
    "knows": "http://schema.org/knows"
  },
  "@id": "http://example.org/alice",
  "@type": "Person",
  "name": "Alice Martin",
  "email": "alice@example.org",
  "url": "http://alice.example.org",
  "knows": [
    "http://example.org/bob",
    "http://example.org/carol"
  ]
}

Partie 2 – Parser du JSON-LD en graphe RDF

Le parsing JSON-LD -> RDF est gere par JsonLdParser. Le parser applique l’expansion (chaque terme est resolu vers son IRI complet) puis cree les triplets. Il lit le document dans un TripleStore (JSON-LD = format dataset) — le helper LoadJsonLd extrait le graphe par defaut.

Processus d’expansion (en 4 étapes) : 1. JSON brut est parse en arbre JSON. 2. @context est evalue (termes resolus vers IRI). 3. Les types (@type) sont convertis en triplets rdf:type. 4. Les references (@id) deviennent des sujets/objets.

Note de portee : JsonLdParser respecte la specification JSON-LD 1.1, y compris les features avancees (scoped contexts, @nest, @propagate).

// --- Parser JSON-LD -> TripleStore -> graphe par defaut (dotNetRDF JsonLdParser) ---
var jsonLdDoc = person.ToString(Formatting.Indented);
var g = LoadJsonLd(jsonLdDoc);
Show("Triplets RDF parses", g.Triples.Count);
Show("Graphe (Turtle)", SerializeToTurtle(g));

// On verifie les termes compacts -> IRIs complets : name doit pointer vers schema:name
var nameTriples = g.GetTriplesWithPredicate(g.CreateUriNode(new Uri("http://schema.org/name")));
Show("Valeur de schema:name", string.Join(", ", nameTriples.Select(t => t.Object.ToString())));
Triplets RDF parses: 6
Graphe (Turtle): @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.

<http://example.org/alice> <http://schema.org/email> "alice@example.org";
                           <http://schema.org/knows> "http://example.org/bob",
                                                     "http://example.org/carol";
                           <http://schema.org/name> "Alice Martin";
                           <http://schema.org/url> <http://alice.example.org/>;
                           a <http://schema.org/Person>.
Valeur de schema:name: Alice Martin^^http://www.w3.org/2001/XMLSchema#string

Lecture : l’expansion en action — 6 triplets

Le parser a produit 6 triplets RDF depuis le document compact. La sortie Turtle révèle le mécanisme central de JSON-LD : l’expansion. Le terme court "name" (défini dans le @context) a été expansé en son IRI complet http://schema.org/name, "Person" en http://schema.org/Person : le document lisible par un humain (clés courtes) a été transformé en un graphe RDF canonique (IRIs globaux univoques) — compatibilité Linked Data sans alourdir la syntaxe JSON.

Pourquoi 6 et pas 4 ? Les quatre champs explicites donnent : name (1), email (1), url (1), knows (2 — le JArray multi-valué produit un triplet par valeur), plus le rdf:type implicite depuis @type: Person (1) = 6.

Le piège littéral vs IRI, visible dans la sortie : schema:url est un IRI (<http://alice.example.org/>, sans guillemets) car le contexte déclare "url": {"@id": ..., "@type": "@id"} — tandis que schema:knows reste un littéral ("http://example.org/bob", avec guillemets) car le contexte mappe knows sans @type: @id. La vérification finale le confirme : schema:name renvoie "Alice Martin^^xsd:string" — le littéral porte son type XSD, preuve que le graphe a capturé la sémantique complète, pas juste du texte brut.

Partie 3 – Serialiser un graphe RDF en JSON-LD

La serialisation RDF -> JSON-LD est l’inverse du parsing. Le JsonLdWriter produit la forme etendue : - Chaque triplet est explicite. - Les URI ne sont pas compactees (pas de @context). - Les valeurs sont enveloppees dans @value.

La forme etendue produite par le writer ressemble a :

[
  {
    "@id": "http://schema.org/alice",
    "@type": ["http://schema.org/Person"],
    "http://schema.org/name": [{"@value": "Alice"}],
    "http://schema.org/knows": [{"@id": "http://schema.org/bob"}]
  }
]

Pourquoi la forme etendue : - Non-ambigue : chaque IRI est explicite. - Standard : conforme a JSON-LD 1.1 (algorithme de serialisation). - Lisibilite pour machines : facile a parser programmatiquement.

Forme compactee (avec @context), celle qu’on ecrirait a la main :

{
  "@context": { "@vocab": "http://schema.org/" },
  "@id": "http://schema.org/alice",
  "@type": "Person",
  "name": "Alice",
  "knows": { "@id": "http://schema.org/bob" }
}

Note de portee : pour la forme compactee, il faut post-traiter la sortie du writer (ajout d’un @context) — c’est l’objet de l’exercice 1.

// --- Construire un graphe RDF a la main, puis le serialiser en JSON-LD ---
var g2 = new Graph();
g2.NamespaceMap.AddNamespace("schema", new Uri("http://schema.org/"));
g2.NamespaceMap.AddNamespace("xsd", new Uri("http://www.w3.org/2001/XMLSchema#"));
var alice2 = g2.CreateUriNode("schema:alice");
var personType = g2.CreateUriNode("schema:Person");
var nameP = g2.CreateUriNode("schema:name");
var knowsP = g2.CreateUriNode("schema:knows");
var bob2 = g2.CreateUriNode("schema:bob");
g2.Assert(new Triple(alice2, g2.CreateUriNode("rdf:type"), personType));
g2.Assert(new Triple(alice2, nameP, g2.CreateLiteralNode("Alice", new Uri("http://www.w3.org/2001/XMLSchema#string"))));
g2.Assert(new Triple(alice2, knowsP, bob2));

Show("Graphe -> JSON-LD (forme etendue)", SerializeToJsonLd(g2));
Show("Graphe -> Turtle (comparaison)", SerializeToTurtle(g2));
Graphe -> JSON-LD (forme etendue): [
  {
    "@id": "http://schema.org/alice",
    "@type": [
      "http://schema.org/Person"
    ],
    "http://schema.org/name": [
      {
        "@value": "Alice"
      }
    ],
    "http://schema.org/knows": [
      {
        "@id": "http://schema.org/bob"
      }
    ]
  }
]
Graphe -> Turtle (comparaison): @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@prefix schema: <http://schema.org/>.

schema:alice schema:knows schema:bob;
             schema:name "Alice";
             a schema:Person.

Partie 4 – Explorer le vocabulaire Schema.org

Schema.org est le vocabulaire le plus utilise sur le Web semantique (10+ millions de sites : Google Search, Knowledge Graph, etc.), projet commun de Google, Microsoft, Yahoo et Yandex. Il couvre : - Personnes : Person, name, email, knows, worksFor. - Produits : Product, name, description, brand, offers. - Evenements : Event, startDate, location, performer. - Organisations : Organization, founder, employee, member.

Pourquoi Schema.org est canonique : - Couverture : 800+ types et 1400+ proprietes. - Adoption : Google, Bing, Yahoo, Yandex (projet commun). - SEO : les moteurs de recherche utilisent Schema.org pour les rich snippets.

On construit un document Schema.org Product (avec une offre imbriquee et une note agregee) et on le valide en le parsant : les triplets generes et les types rencontres sont affiches par la cellule.

// --- Document Schema.org Product en JSON-LD ---
var product = new JObject
{
    ["@context"] = new JObject { ["@vocab"] = "http://schema.org/" },
    ["@type"] = "Product",
    ["@id"] = "http://shop.example.org/produit/42",
    ["name"] = "Casque audio ANC",
    ["description"] = "Casque sans fil a reduction de bruit active",
    ["brand"] = new JObject { ["@type"] = "Brand", ["name"] = "AudioCo" },
    ["offers"] = new JObject
    {
        ["@type"] = "Offer",
        ["price"] = "199.99",
        ["priceCurrency"] = "EUR",
        ["availability"] = "http://schema.org/InStock"
    },
    ["aggregateRating"] = new JObject
    {
        ["@type"] = "AggregateRating",
        ["ratingValue"] = "4.5",
        ["reviewCount"] = "127"
    }
};
Show("Schema.org Product (JSON-LD)", product.ToString(Formatting.Indented));

// Validation : on parse et on compte les triplets generes
var g3 = LoadJsonLd(product.ToString(Formatting.Indented));
Show("Triplets generes (Product)", g3.Triples.Count);
// Les types distincts presents dans le graphe
var types = g3.Triples
    .Where(t => t.Predicate.ToString().EndsWith("type") || t.Predicate.ToString().EndsWith("#type"))
    .Select(t => t.Object.ToString())
    .Distinct();
Show("Types Schema.org rencontres", string.Join(", ", types));
// Le prix et la devise
var priceTriples = g3.GetTriplesWithPredicate(g3.CreateUriNode(new Uri("http://schema.org/price")));
Show("Offre (price)", string.Join(", ", priceTriples.Select(t => t.Object.ToString())));
Schema.org Product (JSON-LD): {
  "@context": {
    "@vocab": "http://schema.org/"
  },
  "@type": "Product",
  "@id": "http://shop.example.org/produit/42",
  "name": "Casque audio ANC",
  "description": "Casque sans fil a reduction de bruit active",
  "brand": {
    "@type": "Brand",
    "name": "AudioCo"
  },
  "offers": {
    "@type": "Offer",
    "price": "199.99",
    "priceCurrency": "EUR",
    "availability": "http://schema.org/InStock"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.5",
    "reviewCount": "127"
  }
}
Triplets generes (Product): 15
Types Schema.org rencontres: http://schema.org/Product, http://schema.org/AggregateRating, http://schema.org/Brand, http://schema.org/Offer
Offre (price): 199.99^^http://www.w3.org/2001/XMLSchema#string

Lecture : Schema.org Product valide

Le document (Casque audio ANC, 199.99 EUR, marque AudioCo, offre InStock, note agregee 4.5/127 avis) se parse en 15 triplets. La sortie confirme que les types imbriqués (Product, Brand, Offer, AggregateRating) ont tous été correctement résolus — la structure JSON imbriquée est devenue un ensemble de triplets RDF liés par leurs @id, et l’offre est interrogeable (schema:price -> 199.99).

C’est l’usage industriel canonique de JSON-LD : les rich snippets Google (étoiles de notation, prix, disponibilité dans les résultats de recherche) sont précisément des blocs <script type="application/ld+json"> Schema.org parsés ainsi — le moteur de recherche reconstruit le graphe exactement comme dotNetRDF vient de le faire.

Note de portee : Schema.org couvre 800+ types (Thing, Person, Organization, Place, Event, Product…), suffisant pour la grande majorité des cas d’usage courants.

Partie 5 – @graph : regrouper plusieurs ressources

Le mot-cle @graph permet de regrouper plusieurs ressources sous un même @context, sans que le noeud racine ne soit lui-même une ressource nommee. C’est la forme naturelle pour les datasets (plusieurs sujets decrits ensemble).

Pourquoi @graph est utile : - Dataset : un seul fichier JSON-LD contient toutes les ressources. - Relations : on peut exprimer des relations entre ressources (alice knows bob). - Validation : un seul parse suffit a charger tout le dataset.

A observer en executant la cellule : le dataset a trois personnes — comptez les triplets attendus (3 rdf:type + 3 schema:name + 1 lien schema:knows = 7), et notez que knows est ici un JObject {"@id": ...} : il s’expand en IRI, contrairement au knows littéral de la Partie 2.

Note de portee : @graph est standard JSON-LD 1.0/1.1, largement supporte par les implementations.

// --- @graph : dataset de plusieurs ressources ---
var dataset = new JObject
{
    ["@context"] = new JObject { ["@vocab"] = "http://schema.org/" },
    ["@graph"] = new JArray
    {
        new JObject { ["@id"] = "http://example.org/alice", ["@type"] = "Person", ["name"] = "Alice", ["knows"] = new JObject { ["@id"] = "http://example.org/bob" } },
        new JObject { ["@id"] = "http://example.org/bob", ["@type"] = "Person", ["name"] = "Bob" },
        new JObject { ["@id"] = "http://example.org/carol", ["@type"] = "Person", ["name"] = "Carol" }
    }
};
Show("Dataset @graph (3 personnes)", dataset.ToString(Formatting.Indented));

var g4 = LoadJsonLd(dataset.ToString(Formatting.Indented));
Show("Triplets (dataset)", g4.Triples.Count);
// Lien knows : Alice -> Bob
var aliceNode = g4.GetUriNode(new Uri("http://example.org/alice"));
var knowsPred = g4.GetUriNode(new Uri("http://schema.org/knows"));
var knowsTriples = g4.GetTriplesWithSubjectPredicate(aliceNode, knowsPred);
Show("Alice knows", string.Join(", ", knowsTriples.Select(t => t.Object.ToString())));
Dataset @graph (3 personnes): {
  "@context": {
    "@vocab": "http://schema.org/"
  },
  "@graph": [
    {
      "@id": "http://example.org/alice",
      "@type": "Person",
      "name": "Alice",
      "knows": {
        "@id": "http://example.org/bob"
      }
    },
    {
      "@id": "http://example.org/bob",
      "@type": "Person",
      "name": "Bob"
    },
    {
      "@id": "http://example.org/carol",
      "@type": "Person",
      "name": "Carol"
    }
  ]
}
Triplets (dataset): 7
Alice knows: http://example.org/bob

Partie 6 – Round-trip JSON-LD <-> Turtle

Le round-trip verifie que la serialisation est isomorphe (memes triplets apres aller-retour) : parser du JSON-LD, serialiser en Turtle, re-parser, et verifier qu’on retrouve le même graphe. C’est une propriete essentielle pour les formats RDF.

Pourquoi le round-trip est important : - Compatibilite : les outils peuvent inter-operer (JSON-LD vers l’un, Turtle vers l’autre). - Validation : un round-trip reussi prouve que le parser/writer sont conformes. - Tests : on peut tester les implementations avec des round-trips.

L’isomorphisme se verifie par egalite de l’ensemble des triplets (sujet, predicat, objet) — la cellule suivante construit un Event, l’envoie en Turtle, le re-parse et compare.

// --- Round-trip JSON-LD -> Turtle -> JSON-LD ---
var original = new JObject
{
    ["@context"] = new JObject { ["@vocab"] = "http://schema.org/" },
    ["@id"] = "http://example.org/workshop",
    ["@type"] = "Event",
    ["name"] = "Atelier JSON-LD",
    ["startDate"] = "2026-07-15"
};
var g5 = LoadJsonLd(original.ToString(Formatting.Indented));
Show("Triplets originaux", g5.Triples.Count);

// Turtle intermediaire
var turtle5 = SerializeToTurtle(g5);
Show("Turtle intermediaire", turtle5);

// Re-parse du Turtle -> nouveau graphe
var g5b = new Graph();
new TurtleParser().Load(g5b, new StringReader(turtle5));
Show("Triplets apres round-trip", g5b.Triples.Count);

// Isomorphisme : memes triplets (S,P,O) dans les deux graphes ?
var same = g5.Triples.Count == g5b.Triples.Count
    && g5.Triples.All(t => g5b.Triples.Any(u => u.Subject.Equals(t.Subject) && u.Predicate.Equals(t.Predicate) && u.Object.Equals(t.Object)));
Show("Round-trip JSON-LD -> Turtle coherent ?", same ? "OUI (isomorphe)" : "NON");

// Re-serialisation JSON-LD pour fermer la boucle
Show("Re-serialisation JSON-LD (forme etendue)", SerializeToJsonLd(g5b));
Triplets originaux: 3
Turtle intermediaire: @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.

<http://example.org/workshop> <http://schema.org/name> "Atelier JSON-LD";
                              <http://schema.org/startDate> "2026-07-15";
                              a <http://schema.org/Event>.
Triplets apres round-trip: 3
Round-trip JSON-LD -> Turtle coherent ?: OUI (isomorphe)
Re-serialisation JSON-LD (forme etendue): [
  {
    "@id": "http://example.org/workshop",
    "http://schema.org/name": [
      {
        "@value": "Atelier JSON-LD"
      }
    ],
    "http://schema.org/startDate": [
      {
        "@value": "2026-07-15"
      }
    ],
    "@type": [
      "http://schema.org/Event"
    ]
  }
]

Lecture : round-trip isomorphe — OUI

Le test de conformité réussit : JSON-LD → Turtle → re-parse → mêmes 3 triplets, verdict OUI (isomorphe). La sérialisation intermédiaire Turtle n’a ni perdu ni ajouté d’information : name, startDate, rdf:type se retrouvent à l’identique dans les deux graphes.

L’honnêteté des littéraux, visible dans la sortie : startDate "2026-07-15" reste une chaîne littérale (le @context de l’exemple ne déclare pas de datatype) — le round-trip la préserve telle quelle, sans la promouvoir en xsd:date. L’isomorphisme se vérifie par égalité des ensembles de triplets (sujet, prédicat, objet), pas par comparaison de texte : l’ordre des triplets et les préfixes peuvent différer sans que le graphe change.

Ce round-trip est le test de robustesse standard pour tout moteur RDF : si un format altère le graphe (pertes de littéraux typés, fusion/séparation de nœuds), le round-trip l’expose immédiatement. Le verdict OUI atteste que dotNetRDF traite JSON-LD et Turtle comme deux vues fidèles du même modèle abstrait (le graphe RDF orienté étiqueté).

Conclusion

Ce twin C# a couvert les 6 piliers de JSON-LD avec dotNetRDF : 1. @context : dictionnaire terme -> IRI (Partie 1). 2. Parsing JSON-LD -> graphe RDF via JsonLdParser (Partie 2). 3. Serialisation graphe -> JSON-LD via JsonLdWriter, forme etendue (Partie 3). 4. Schema.org : vocabulaire de données structurees (Partie 4). 5. @graph : regroupement de ressources (Partie 5). 6. Round-trip JSON-LD <-> Turtle + isomorphisme (Partie 6).

Cas d’usage industriels : - SEO : Schema.org -> rich snippets dans Google Search. - API REST : JSON-LD comme format d’echange (Content-Type: application/ld+json). - Knowledge Graphs : representation des entites dans Wikidata, DBpedia, Google KG. - Linked Data Platform (LDP) : protocole W3C pour publier des donnees liees en JSON-LD.

Pour aller plus loin : - JSON-LD 1.1 : scoped contexts, @nest, @propagate (features avancees). - Framing : extraction d’un sous-graphe typé (cf. exercice 2). - Compaction : ajout d’un @context pour produire une forme compactee (cf. exercice 1). - Validation : SHACL shapes pour valider la structure d’un document JSON-LD.

Le notebook Python original utilise rdflib + pyld ; ce twin C# utilise dotNetRDF (VDS.RDF.*). Les deux sont des moteurs W3C complets sur le même standard JSON-LD 1.1 (comparaison cross-langage, EPIC #4956 Prong B).


Exercices à compléter

Ces exercices prolongent les 6 piliers ci-dessus avec dotNetRDF. Les stubs utilisent des marqueurs // TODO etudiant — à compléter. Chaque exercice est précédé de son contexte (objectif + notion JSON-LD + API dotNetRDF).

Exercice 1 – Compaction avec un @context personnalise

Objectif. Écrire un @context qui compacte un graphe de livres : remplacer les IRIs verbeux (http://schema.org/title, .../author, .../datePublished) par des termes courts français (titre, auteur, publie), puis l’utiliser pour construire un document JSON-LD compact lisible par un humain.

Notion JSON-LD – la compaction. La compaction est l’inverse de l’expansion : partant d’un graphe RDF (ou d’un JSON-LD expnase), elle applique un @context pour produire le document JSON le plus court et lisible possible, où chaque propriété porte un terme court au lieu de son IRI complet. C’est ce que publie un site web dans sa balise <script type="application/ld+json"> : le terme "name" plutôt que "http://schema.org/name".

API dotNetRDF. On construit manuellement le @context comme un JObject ({"@context": {"titre": "http://schema.org/title", ...}}) — la même structure qu’à la Partie 1. Le JsonLdProcessor de dotNetRDF sait compacter un graphe, mais l’exercice se concentre sur la définition du contexte, qui est le cœur pédagogique.

Sortie attendue (une fois le contexte defini) : un document compact du type

{
  "@context": { "titre": "http://schema.org/title", "auteur": "http://schema.org/author", "publie": "http://schema.org/datePublished" },
  "@id": "http://example.org/book/42",
  "titre": "Le Comte de Monte-Cristo"
}

Indices. Étape 1 : mapper chaque terme court (titre/auteur/publie) vers son IRI Schema.org. Étape 2 : vérifier qu’un document construit avec ce contexte se parse bien en graphe RDF (round-trip).

Difficulte : moyenne.

// Exercice 1 : Compaction avec un contexte personnalise
// TODO etudiant : ecrivez un contexte qui compacte un graphe de livres
// en utilisant les termes courts "titre", "auteur", "publie".
// Indice : definissez un JObject @context mappant chaque terme court a son IRI complet.
// Etape 1 : ecrire le contexte. Etape 2 : l'utiliser pour construire un document JSON-LD compact.
public static JObject BuildBiblioContext()
{
    // TODO etudiant : retournez un contexte du type {"@context": {"titre": "http://.../title", ...}}
    return new JObject();
}
Show("Contexte biblio (a completer)", BuildBiblioContext().ToString());
Contexte biblio (a completer): {}

Tant que BuildBiblioContext n’est pas complete, la sortie reste Contexte biblio (a completer): {}. Note : le JsonLdWriter de dotNetRDF produit toujours la forme etendue — la compaction se joue ici entierement dans la definition du @context, exactement comme en Partie 1.

Exercice 2 – Framing : extraire un sous-graphe type

Objectif. Extraire d’un graphe RDF tous les noeuds de type schema:Person et retourner pour chacun son @id et son schema:name. C’est l’operation de framing — selectionner un sous-graphe par sa forme (type, proprietes).

Notion JSON-LD – le framing. Le framing (mot-cle @frame) selectionne dans un graphe la partie qui matche un patron (« tous les noeuds de type Person avec leur nom »). En pratique, on l’emule souvent en enumerant les triples et en filtrant — c’est l’approche legere de cet exercice, qui evite d’introduire toute la mecanique du JsonLdFrame.

API dotNetRDF. Le graphe g4 (construit en Partie 5, dataset multi-ressources via @graph) expose .Triples : enumerez-le, filtrez les triples de predicat rdf:type dont l’objet est http://schema.org/Person, puis pour chaque sujet trouve recuperez son schema:name. Lien direct avec la Partie 4 (vocabulaire Schema.org) et la Partie 5 (@graph).

Sortie attendue (une fois complete) : les trois personnes du dataset de la Partie 5, chacune avec son @id et son nom (alice/Alice, bob/Bob, carol/Carol).

Indices. Etape 1 : identifier les sujets ?s rdf:type schema:Person. Etape 2 : pour chacun, lire le triple ?s schema:name ?nom.

Difficulte : moyenne-avancee.

// Exercice 2 : Framing (extraire un sous-graphe type)
// TODO etudiant : implementez une selection qui extrait tous les nœuds
// de type schema:Person dans un graphe et retourne leur @id + nom.
// Indice : enumeratez g4.Triples, filtrez par predicat rdf:type == http://schema.org/Person.
// Etape 1 : trouver les sujets de type Person. Etape 2 : pour chacun, recuperer son schema:name.
public static List<JObject> ExtractPersons(IGraph g)
{
    // TODO etudiant : retournez la liste des {"@id":..., "name":...}
    return new List<JObject>();
}
var persons = ExtractPersons(g4);
Show("Personnes extraites (a completer)", persons.Count == 0 ? "0 (a completer)" : string.Join("; ", persons.Select(p => p.ToString())));
Personnes extraites (a completer): 0 (a completer)

Exercice 3 – Valider un JSON-LD mal forme

Objectif. Écrire une fonction IsValidJsonLd(string json) qui retourne true si la chaîne est du JSON-LD parsable en graphe RDF, false sinon. Testez avec un document correct et un document volontairement casse (@context invalide, JSON mal forme) : le parser doit lever une exception descriptive.

Notion JSON-LD – qu’est-ce qu’un JSON-LD « valide » ? « Valide » ici signifie parsable : la chaîne est du JSON bien formé ET son contenu se traduit en triples RDF. Ce n’est pas une garantie de justesse sémantique — un document peut être syntaxiquement valide mais décrire des données absurdes. La distinction parsabilité (mécanique) vs correction (sémantique) est exactement celle qu’on rencontre en XML (well-formed vs valid).

API dotNetRDF. Le JsonLdParser lève une exception (RdfParseException ou JsonReaderException) si le JSON est cassé ou n’est pas du JSON-LD exploitable. D’ou le pattern try { parser.Load(...); return true; } catch { return false; }. Lien avec la Partie 2 (parser JSON-LD -> graphe).

Sortie attendue (une fois l’exercice complete) :

JSON-LD valide (cas correct): True
JSON-LD valide (cas casse): False

(Tant que le stub n’est pas complete, la cellule emet son marqueur et False pour les deux cas — valeur neutre du stub, pas un verdict du parser.)

Indices. Encapsulez l’appel LoadJsonLd (qui invoque JsonLdParser.Load) dans un try/catch. Testez avec un cas correct ({"@id":...}) et un cas casse ({ce n'est pas du json}).

Difficulte : moyenne. Pour aller plus loin : validation stricte avec un wrapper qui catch les exceptions specifiques.

// Exercice 3 : Valider un JSON-LD mal forme
// TODO etudiant : ecrivez une fonction qui tente de parser un JSON-LD et
// retourne true s'il est valide (parsable en graphe RDF), false sinon.
// Indice : try/catch autour de LoadJsonLd (qui appelle JsonLdParser.Load).
public static bool IsValidJsonLd(string json)
{
    // TODO etudiant : retournez vrai si parsable, faux sinon
    return false;
}
// Stub non complete : `false` est une valeur neutre (indiscernable de "pas fait"),
// pas un verdict du parser -- les deux lignes ci-dessous ne renseignent donc pas
// sur la tolerance de dotNetRDF ni sur la validite des deux documents.
Console.WriteLine("Exercice a completer : IsValidJsonLd");
Show("JSON-LD valide (cas correct)", IsValidJsonLd("{\"@id\":\"http://x.org/a\",\"@type\":\"http://x.org/T\",\"http://x.org/name\":\"foo\"}"));
Show("JSON-LD valide (cas casse)", IsValidJsonLd("{ce n'est pas du json}"));
Exercice a completer : IsValidJsonLd
JSON-LD valide (cas correct): False
JSON-LD valide (cas casse): False

Twin C# du notebook Python SW-9-Python-JSONLD.ipynb. Marathon parite .NET <-> Python (#4956, Prong B). dotNetRDF 3.4.0 (vrai moteur W3C JSON-LD 1.1).

Retour au sommet