概要

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 ライブラリが必要です:

npm install n3
npm install --save-dev @types/n3

まとめ

項目内容
確認された挙動一部のURI参照が { "@id": "..." } ではなく文字列になる
発生条件全てのケースではなく、コンテキストやプロパティにより異なる
回避策TTL形式で取得し、n3でパースしてJSON-LD構築
追加依存n3

Dydraを使用する際、JSON-LDの出力形式が期待と異なる場合は、TTL形式で取得してクライアント側で変換する方法が有効です。

参考リンク


Last updated: 2025-12-29