Dydra JSON-LDシリアライゼーションの挙動と回避策
概要 Dydraは優れたクラウドベースのRDFトリプルストアですが、JSON-LDシリアライゼーションにおいて、一部のケースで期待と異なる出力が得られることがあります。このブログでは、その挙動と、我々が実装した回避策について解説します。
確認された挙動 期待される出力 JSON-LD仕様では、URI参照は以下のようにオブジェクト形式で出力されることが一般的です:
{ "@id": "https://example.com/item/1", "@type": ["prov:Entity"], "prov:wasAttributedTo": { "@id": "https://sepolia.etherscan.io/address/0x1234..." }, "prov:wasGeneratedBy": { "@id": "https://sepolia.etherscan.io/tx/0xabcd..." } } Dydraで確認された出力 DydraのJSON-LDエンドポイントでは、一部のURI参照が単なる文字列として出力されるケースが確認されました:
{ "@id": "https://example.com/item/1", "@type": ["prov:Entity"], "prov:wasAttributedTo": "https://sepolia.etherscan.io/address/0x1234...", "prov:wasGeneratedBy": "https://sepolia.etherscan.io/tx/0xabcd..." } 注意 : この挙動は全てのプロパティで発生するわけではなく、@contextの定義やプロパティの種類によって異なる場合があります。
挙動の違いによる影響 形式 JSON-LDパーサーの解釈 { "@id": "..." } URI参照(他ノードへのリンク) "..." リテラル文字列 この違いにより、以下の影響が生じる可能性があります:
グラフ構造のトラバーサルに影響 一部のSPARQLクエリ結果に影響 JSON-LDフレーミング処理に影響 型付きリテラルについて 同様に、xsd:dateTime などの型付きリテラルでも型情報が省略されるケースがあります。
期待される出力 :
{ "prov:startedAtTime": { "@value": "2025-01-15T10:30:00Z", "@type": "xsd:dateTime" } } 確認された出力 :
{ "prov:startedAtTime": "2025-01-15T10:30:00Z" } 回避策 アプローチ:TTL形式で取得してJSON-LDを構築 DydraはTurtle (TTL) 形式では正確にシリアライズするため、以下の戦略を採用しました:
[クライアント] │ │ Accept: text/turtle v [Dydra SPARQL Endpoint] │ │ TTL形式で返却 v [n3パーサー] │ │ Quadsに変換 v [JSON-LD構築ロジック] │ │ 正しいJSON-LD v [アプリケーション] 実装 import { Parser } from "n3"; /** * TTLをパースしてJSON-LDに変換 * DydraのJSON-LDシリアライゼーションの挙動を回避 */ function turtleToJsonLd(turtle: string): RDFGraph { const parser = new Parser(); const quads = parser.parse(turtle); // Subject別にトリプルをグループ化 const subjects = new Map<string, Map<string, unknown[]>>(); for (const quad of quads) { const subjectId = quad.subject.value; if (!subjects.has(subjectId)) { subjects.set(subjectId, new Map()); } const predicates = subjects.get(subjectId)!; const predicateId = quad.predicate.value; if (!predicates.has(predicateId)) { predicates.set(predicateId, []); } // オブジェクトの値を型情報付きで構築 let objectValue: unknown; if (quad.object.termType === "NamedNode") { // URI参照: { "@id": "..." } ← ここがポイント objectValue = { "@id": quad.object.value }; } else if (quad.object.termType === "Literal") { const literal = quad.object; if (literal.language) { // 言語タグ付きリテラル objectValue = { "@value": literal.value, "@language": literal.language }; } else if (literal.datatype && literal.datatype.value !== "http://www.w3.org/2001/XMLSchema#string") { // 型付きリテラル(xsd:string以外) objectValue = { "@value": literal.value, "@type": literal.datatype.value }; } else { // プレーンリテラル objectValue = literal.value; } } else if (quad.object.termType === "BlankNode") { objectValue = { "@id": `_:${quad.object.value}` }; } else { objectValue = quad.object.value; } predicates.get(predicateId)!.push(objectValue); } // JSON-LD @graphを構築 const graph: Array<Record<string, unknown>> = []; for (const [subjectId, predicates] of subjects) { const node: Record<string, unknown> = { "@id": subjectId }; for (const [predicateId, objects] of predicates) { if (predicateId === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") { // @typeは特別扱い node["@type"] = objects.map((o) => { if (typeof o === "object" && o !== null && "@id" in o) { return (o as { "@id": string })["@id"]; } return o; }); } else { // 単一値の場合は配列から取り出す node[predicateId] = objects.length === 1 ? objects[0] : objects; } } graph.push(node); } return { "@context": JSONLD_CONTEXT, "@graph": graph, }; } 使用例 // TTL形式で取得して変換 const response = await fetch(`${DYDRA_ENDPOINT}/sparql`, { method: "POST", headers: { "Accept": "text/turtle", // TTLで取得 }, body: query, }); const turtle = await response.text(); const jsonld = turtleToJsonLd(turtle); // JSON-LDに変換 依存ライブラリ この回避策には n3 ライブラリが必要です:
...
2025年12月29日 · 更新: 2025年12月29日 · 3 分 · Nakamura