Overview

Dydra is an excellent cloud-based RDF triple store, but in some cases its JSON-LD serialization may produce output that differs from expectations. This blog post explains the observed behavior and the workaround we implemented.

Observed Behavior

Expected Output

In the JSON-LD specification, URI references are commonly output in object form as follows:

{
  "@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..."
  }
}

Output Observed in Dydra

In Dydra’s JSON-LD endpoint, some URI references were observed to be output as plain strings:

{
  "@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..."
}

Note: This behavior does not occur for all properties and may vary depending on the @context definition and property types.

Impact of Behavioral Differences

FormatJSON-LD Parser Interpretation
{ "@id": "..." }URI reference (link to another node)
"..."Literal string

This difference can cause the following impacts:

  • Affects graph structure traversal
  • Affects some SPARQL query results
  • Affects JSON-LD framing processing

Regarding Typed Literals

Similarly, type information may be omitted for typed literals such as xsd:dateTime.

Expected output:

{
  "prov:startedAtTime": {
    "@value": "2025-01-15T10:30:00Z",
    "@type": "xsd:dateTime"
  }
}

Observed output:

{
  "prov:startedAtTime": "2025-01-15T10:30:00Z"
}

Workaround

Approach: Fetch in TTL Format and Build JSON-LD

Since Dydra serializes Turtle (TTL) format accurately, the following strategy was adopted:

[Client]
    |
    | Accept: text/turtle
    v
[Dydra SPARQL Endpoint]
    |
    | Returns in TTL format
    v
[n3 Parser]
    |
    | Converts to Quads
    v
[JSON-LD Construction Logic]
    |
    | Correct JSON-LD
    v
[Application]

Implementation

import { Parser } from "n3";

/**
 * Parse TTL and convert to JSON-LD
 * Workaround for Dydra's JSON-LD serialization behavior
 */
function turtleToJsonLd(turtle: string): RDFGraph {
  const parser = new Parser();
  const quads = parser.parse(turtle);

  // Group triples by 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, []);
    }

    // Build object values with type information
    let objectValue: unknown;
    if (quad.object.termType === "NamedNode") {
      // URI reference: { "@id": "..." } <- This is the key point
      objectValue = { "@id": quad.object.value };
    } else if (quad.object.termType === "Literal") {
      const literal = quad.object;
      if (literal.language) {
        // Language-tagged literal
        objectValue = { "@value": literal.value, "@language": literal.language };
      } else if (literal.datatype &&
                 literal.datatype.value !== "http://www.w3.org/2001/XMLSchema#string") {
        // Typed literal (other than xsd:string)
        objectValue = { "@value": literal.value, "@type": literal.datatype.value };
      } else {
        // Plain literal
        objectValue = literal.value;
      }
    } else if (quad.object.termType === "BlankNode") {
      objectValue = { "@id": `_:${quad.object.value}` };
    } else {
      objectValue = quad.object.value;
    }

    predicates.get(predicateId)!.push(objectValue);
  }

  // Build 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") {
        // Special handling for @type
        node["@type"] = objects.map((o) => {
          if (typeof o === "object" && o !== null && "@id" in o) {
            return (o as { "@id": string })["@id"];
          }
          return o;
        });
      } else {
        // Extract from array for single values
        node[predicateId] = objects.length === 1 ? objects[0] : objects;
      }
    }

    graph.push(node);
  }

  return {
    "@context": JSONLD_CONTEXT,
    "@graph": graph,
  };
}

Usage Example

// Fetch in TTL format and convert
const response = await fetch(`${DYDRA_ENDPOINT}/sparql`, {
  method: "POST",
  headers: {
    "Accept": "text/turtle",  // Fetch as TTL
  },
  body: query,
});

const turtle = await response.text();
const jsonld = turtleToJsonLd(turtle);  // Convert to JSON-LD

Required Dependencies

This workaround requires the n3 library:

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

Summary

ItemDetails
Observed behaviorSome URI references become strings instead of { "@id": "..." }
Occurrence conditionsNot all cases; varies by context and properties
WorkaroundFetch in TTL format, parse with n3, and build JSON-LD
Additional dependencyn3

When using Dydra, if the JSON-LD output format differs from expectations, fetching in TTL format and converting on the client side is an effective approach.


Last updated: 2025-12-29