We adapted the SPARQL Explorer “Snorql”, originally built for Virtuoso and Dydra, to also work with Apache Jena Fuseki. Although SPARQL is a W3C standard, behavioral differences between endpoint implementations are surprisingly significant. This article documents the three issues we encountered during Fuseki support and their solutions.

Development Environment

We launched Fuseki with Docker and tested locally.

# docker-compose.yml
services:
  fuseki:
    image: stain/jena-fuseki
    container_name: fuseki
    ports:
      - "3030:3030"
    environment:
      - ADMIN_PASSWORD=admin
      - FUSEKI_DATASET_1=test
    volumes:
      - fuseki-data:/fuseki

volumes:
  fuseki-data:
docker compose up -d

# Load test data
curl -X POST 'http://localhost:3030/test/data' \
  -H 'Content-Type: text/turtle' \
  --data-binary @testdata.ttl

1. Different Response Format for DESCRIBE

Symptom

When sending a DESCRIBE query to Fuseki, results were not displayed on screen. JSON parse errors appeared in the console.

Investigation

SPARQL’s DESCRIBE / CONSTRUCT, unlike SELECT, return RDF graphs. The format varies by endpoint.

EndpointDESCRIBE responseoutput= parameter
Virtuoso / DydraSPARQL Results JSON (proprietary extension)
FusekiRDF format (Turtle, RDF/JSON, etc.)Takes precedence over Accept header

Snorql had been appending output=json as a URL parameter. Fuseki interprets this and returns JSON-LD, but Snorql expects RDF/JSON, causing the parse to break.

Using curl to isolate the issue makes the cause clear.

# Without output parameter -> Accept header takes effect -> Returns RDF/JSON
curl -H 'Accept: application/rdf+json' \
  'http://localhost:3030/test/sparql?query=DESCRIBE+'
# -> Content-Type: application/rdf+json

# With output=json -> Accept header is ignored -> Returns JSON-LD
curl -H 'Accept: application/rdf+json' \
  'http://localhost:3030/test/sparql?output=json&query=DESCRIBE+'
# -> Content-Type: application/ld+json

The output=json parameter was interfering with Fuseki. Relying on Accept header content negotiation returns the correct format.

Fix (5 locations)

sparql.js – Conditional output parameter submission

-urlQueryString += 'output=' + _output + '&';
+if(_output) urlQueryString += 'output=' + _output + '&';

If _output is an empty string, output= is not sent.

sparql.js – JSON detection via Content-Type

+var isJson = _output == 'json' || (xhr.getResponseHeader('Content-Type') || '').match(/json/i);
-    _output == 'json' ? (JSON.parse(xhr.responseText) || ...
+    isJson ? (JSON.parse(xhr.responseText) || ...

Even when the output parameter is not sent, if the response Content-Type contains json, it uses JSON.parse. This handles the case where Fuseki returns application/rdf+json.

snorql.js – Accept header and output control for DESCRIBE

-if(!this.homedef.is_virtuoso) qparam.accept = "application/rdf+json,jsonapplication/json";
+if(!this.homedef.is_virtuoso){
+    qparam.accept = "application/rdf+json,application/json";
+    qparam.output = "";  // Do not send output parameter
+}

Fixed a typo (jsonapplication/json -> ,application/json) and set output to an empty string to enable Fuseki’s content negotiation.

snorql.js – Correctly propagate empty string output

-if(output) service.setOutput(output);
+if(output !== undefined) service.setOutput(output);

JavaScript’s if(output) evaluates the empty string "" as falsy, so we changed it to !== undefined.

snorql_def.js – endpoint_type setting

-endpoint_type: "virtuoso",
+endpoint_type: "fuseki",

2. Queries Fail Without PREFIX Declarations

Symptom

DESCRIBE now displayed correctly, but label retrieval and related resource searches returned 400 errors.

Parse error: Unresolved prefixed name: rdfs:label

Cause

After displaying DESCRIBE results, Snorql internally issues auxiliary queries for label retrieval and similar tasks. These use prefixed names like rdfs:label and schema:name directly.

EndpointPREFIX declarations
VirtuosoImplicitly resolves rdfs: etc. (no declarations needed)
FusekiAll explicit PREFIX declarations required (W3C compliant)

Code that relied on Virtuoso’s implicit resolution did not work on Fuseki.

Fix

snorql_ldb.js – Automatically prepend PREFIXes to auxiliary queries

set_service: function(method){
     if(!this.service){
         this.service = new SPARQL.Service(this.endpoint);
         this.service.setMethod(method || "GET");
         this.service.setRequestHeader("Accept", "application/sparql-results+json,*/*");
         this.service.setOutput("json");
+        // Register all prefixes from namespaces.js -> auto-prepended on query submission
+        var ns = this.app.snql._namespaces;
+        for(var pfx in ns) this.service.setPrefix(pfx, ns[pfx]);
     }
     return this.service;
 },

By registering namespaces via SPARQL.Service.setPrefix(), PREFIX rdfs: <...> is automatically prepended to queries on submission. All prefixes defined in namespaces.js were bulk-registered.

snorql_ldb.js – Null guard

set_uri: function(qname){
+    if(!qname) return "";
     var nslocal = qname.split(":");

Prevents TypeError when an undefined property is passed as undefined.


3. The is_virtuoso Flag Was Doing Two Jobs

Symptom

When trying to run Dydra alongside Fuseki, Dydra started returning 400 errors.

failed to parse after 'define' at offset 0 on line 1.
define input:default-graph-exclude

Cause

The is_virtuoso flag had two different responsibilities.

ResponsibilityLocationWhen truthy
Response formatsnorql.jsSends output=json
Virtuoso-specific syntaxutil.jsAppends define sql:describe-mode "CBD" etc.

Dydra needs output=json (same response format as Virtuoso), but does not accept Virtuoso-specific define directives. Setting is_virtuoso to truthy enabled both, and define caused the error.

Fix: Separation of Concerns

We split one flag into three settings.

SettingRole
endpoint_typeON/OFF for Virtuoso define directives
is_virtuosoResponse format (output=json vs Accept header)
describe_as_constructAutomatic DESCRIBE -> CONSTRUCT conversion

util.js – Use endpoint_type for define directives

-if(!is_virtuoso) return query;
+if(Snorqldef.home?.endpoint_type !== "virtuoso") return query;

util.js – Automatic DESCRIBE to CONSTRUCT conversion

+if(qtype === "DESCRIBE" && Snorqldef.home?.describe_as_construct){
+    var m = query.match(/\bDESCRIBE\s+(]+>)/i);
+    if(m) query = query.replace(/\bDESCRIBE\s+]+>/i,
+        "CONSTRUCT { " + m[1] + " ?p ?o } WHERE { " + m[1] + " ?p ?o }");
+}

Since JPS’s Virtuoso returns a server error (SR452) for DESCRIBE, an option was added to convert DESCRIBE <uri> to an equivalent CONSTRUCT.

Configuration for Each Endpoint

// Virtuoso (JPS)
{ endpoint_type: "virtuoso", is_virtuoso: "default", describe_as_construct: true }

// Dydra
{ endpoint_type: "any", is_virtuoso: "default" }  // output=json enabled, no define

// Fuseki
{ endpoint_type: "fuseki" }  // Content negotiation via Accept header, no define

Data Flow (After Fix)

[SELECT -- All endpoints in common]
  output=json -> SPARQL Results JSON

[DESCRIBE -- Fuseki]
  Accept: application/rdf+json -> No output parameter
  -> Fuseki returns RDF/JSON -> JSON detection via Content-Type -> Parse succeeds

[DESCRIBE -- Dydra]
  endpoint_type != "virtuoso" -> No define
  -> output=json -> SPARQL Results JSON

[DESCRIBE -- JPS (Virtuoso)]
  describe_as_construct -> Converted to CONSTRUCT
  -> output=json -> SPARQL Results JSON

[Auxiliary queries -- Label retrieval etc.]
  All PREFIXes from namespaces.js auto-prepended -> Resolvable on Fuseki

Summary

Although SPARQL is a standard specification, differences between implementations are surprisingly common.

DifferenceVirtuosoFuseki
DESCRIBE responseProprietary JSONW3C-compliant RDF format
PREFIX resolutionImplicit resolutionExplicit declarations required
Proprietary extensions (define)SupportedNot supported (causes parse errors)

To absorb these differences, we separated the dual responsibilities of is_virtuoso into three distinct settings: endpoint_type, is_virtuoso, and describe_as_construct. When adding a new endpoint type, you only need to add flags in snorql_def.js without breaking existing behavior.

For discovering these differences, checking Content-Type with curl and the browser’s Network tab proved most useful. Even with SPARQL that should be the same by specification, response headers reveal the implementation’s personality.