SPARQL: Result Formats
purrdf-sparql-results is the results
boundary of the SPARQL stack: the canonical authority for turning a
SparqlResult (SELECT solutions, ASK boolean, or CONSTRUCT graph) into the
four W3C SPARQL Results formats — JSON (SRJ), XML, CSV, and TSV — plus an
additive, caller-named provenance extension where the format can carry one.
JSON and XML documents can also be read back (from_json, from_xml).
#![allow(unused)]
fn main() {
use purrdf::{RdfDatasetBuilder, TermValue};
use purrdf::sparql::{from_json, serialize, ResultProvenance, SparqlResult, SparqlResultsFormat};
let result = SparqlResult::Solutions {
variables: vec!["s".to_owned()],
rows: vec![vec![Some(TermValue::Iri("https://example.org/alice".to_owned()))]],
aux: RdfDatasetBuilder::new().freeze().expect("empty auxiliary dataset").into(),
};
let outcome = serialize(&result, SparqlResultsFormat::Json, &ResultProvenance::default(), None)
.expect("SELECT serializes to SRJ");
assert!(!outcome.provenance_dropped);
let decoded = from_json(&outcome.bytes).expect("emitted JSON parses back");
assert_eq!(decoded.variables, ["s"]);
assert_eq!(decoded.rows, [vec![Some(TermValue::Iri("https://example.org/alice".to_owned()))]]);
}
Per-format writers (to_json, to_xml, to_csv, to_tsv) and readers
(from_json, from_json_boolean, from_xml, from_xml_boolean) are also
exported directly.
Behavior worth knowing before you pick a format
- Byte-deterministic output — the same result always serializes to the same bytes, like every other PurRDF output path (Codecs & Determinism).
- The support matrix is enforced, not fudged — XML rejects CONSTRUCT
graphs, and CSV/TSV reject both ASK booleans and CONSTRUCT graphs, each as
a typed
Error::Format, rather than emitting something spec-shaped but wrong. - Lossy projections are flagged — CSV/TSV have no extension point, so a
populated provenance is trimmed at the exit gate and
SerializeOutcome::provenance_droppedis set; the drop is never silent. The same flag is set for JSON/XML when a non-emptyResultProvenanceis supplied with noProvenanceNamespaceto anchor it under — see below. - RDF 1.2 base direction uses the spec’s own key — a directional literal’s
base direction serializes as
its:dir(JSON: an additive"its:dir"member beside"value"/"xml:lang"; XML:<literal its:dir="…">), the same spelling the SPARQL 1.2 Query Results specification’s own example uses and the RDF/XML codec already emits. The XML/JSON readers also accept two legacy spellings this crate’s own writer previously produced (a baredirattribute, and — XML only — apurrdf:dir-namespaced one) for backward compatibility, butits:dirwins when more than one is present on the same literal, and onlyits:diris ever written.
| Format | SELECT | ASK | CONSTRUCT | Provenance extension |
|---|---|---|---|---|
| JSON (SRJ) | yes | yes | yes | yes, with a namespace |
| XML | yes | yes | rejected | yes, with a namespace |
| CSV | yes | rejected | rejected | dropped, flagged |
| TSV | yes | rejected | rejected | dropped, flagged |
The provenance extension
The provenance extension is additive and caller-named: a standard
SPARQL results consumer can read the JSON/XML documents unchanged, while a
provenance-aware consumer can recover per-result data carried alongside the
bindings, anchored under an identifier the caller supplies — never a
purrdf-minted one (PurRDF mints no vocabulary IRIs of its own; see
AGENTS.md’s
“NOT an ontology” contract). Supply a ProvenanceNamespace — a prefix (the
bare top-level JSON member key, and the XML namespace prefix) plus the XML
namespace iri — as serialize’s fourth argument (to_json/to_xml take it as
their third: those two have no format parameter to pick).
ProvenanceNamespace::new validates prefix as an XML Namespaces NCName
(rejecting, among other things, :, whitespace, and the reserved xml/
xmlns names) and iri as an absolute IRI, since prefix is spliced
directly into XML element/attribute names and cannot be escaped the way text
content can:
#![allow(unused)]
fn main() {
use purrdf::sparql::{from_json_boolean, ProvenanceNamespace, ResultProvenance, SparqlResult, SparqlResultsFormat, serialize};
let result = SparqlResult::Boolean(true);
let namespace = ProvenanceNamespace::new("prov", "https://example.org/provenance#")
.expect("valid NCName prefix + absolute IRI");
let provenance = ResultProvenance {
query_hash: Some("example-query".to_owned()),
..ResultProvenance::default()
};
let outcome = serialize(&result, SparqlResultsFormat::Json, &provenance, Some(&namespace))
.expect("ASK with caller-named provenance serializes");
assert!(!outcome.provenance_dropped);
assert!(from_json_boolean(&outcome.bytes).expect("emitted ASK parses back"));
let without_namespace = serialize(&result, SparqlResultsFormat::Json, &provenance, None)
.expect("ASK without a provenance namespace serializes");
assert!(without_namespace.provenance_dropped);
}
With namespace: None, JSON/XML emit no provenance element/member at all,
however populated a ResultProvenance is — the same drop-and-flag contract
CSV/TSV always used. Where the format has no extension point at all (CSV/TSV),
the provenance is dropped loudly regardless of namespace, per the loss
discipline described in Slices, Mappings & Provenance.
One term-syntax authority
The crate depends on purrdf-core, purrdf-lex and purrdf-iri, and stays
wasm-clean. Shared lexical readers and IRI validation serve the result formats;
term and N-Triples syntax come from the kernel’s emit primitives and the shared
lexical term writer. Results, codecs and diagnostics therefore use the same
term-syntax authority.
Related
- SPARQL: Querying — producing the
SparqlResultin the first place. - docs.rs/purrdf-sparql-results — the full API reference.