Output Formats
Fluree supports multiple output formats for query results, each optimized for different use cases. You can choose the format that best fits your application’s needs.
Supported Formats
JSON-LD Format
Default format for JSON-LD Query. Provides compact, context-aware JSON with IRI expansion/compaction.
Characteristics:
- Uses
@contextfor IRI compaction - Compact IRIs (e.g.,
ex:aliceinstead of full IRIs) - Inferable datatypes (string, long, double, boolean) rendered as bare values
- Language tags preserved
Example (graph crawl):
[
{
"@id": "ex:alice",
"schema:name": "Alice",
"schema:age": 30,
"schema:knows": {"@id": "ex:bob"}
}
]
Example (tabular SELECT):
[
["Alice", 30],
["Bob", 25]
]
SPARQL JSON Format
Standard SPARQL 1.1 result format for SPARQL queries.
Characteristics:
- W3C SPARQL 1.1 compliant
- Standard
resultsandbindingsstructure - Datatype information included
- Language tags included
Example:
{
"head": {
"vars": ["name", "age"]
},
"results": {
"bindings": [
{
"name": {
"type": "literal",
"value": "Alice"
},
"age": {
"type": "literal",
"value": "30",
"datatype": "http://www.w3.org/2001/XMLSchema#integer"
}
}
]
}
}
Typed JSON Format
Type-preserving JSON format with explicit datatype information on every value. Works with both tabular SELECT queries and graph crawl (entity-centric) queries.
Characteristics:
- Every literal includes
{"@value": ..., "@type": "..."}— even inferable types - References use
{"@id": "..."} - Language-tagged strings use
{"@value": ..., "@language": "..."} @jsonvalues use{"@value": <parsed>, "@type": "@json"}- Nested entities in graph crawl results are also fully typed
- IRIs compacted via
@context
Example (tabular SELECT):
[
{
"?name": {"@value": "Alice", "@type": "xsd:string"},
"?age": {"@value": 30, "@type": "xsd:long"}
}
]
Example (graph crawl):
[
{
"@id": "ex:alice",
"@type": ["schema:Person"],
"schema:name": {"@value": "Alice", "@type": "xsd:string"},
"schema:age": {"@value": 30, "@type": "xsd:long"},
"schema:knows": {
"@id": "ex:bob",
"schema:name": {"@value": "Bob", "@type": "xsd:string"}
},
"ex:data": {"@value": {"key": "val"}, "@type": "@json"}
}
]
Agent JSON Format
Optimized for LLM/agent consumption. Returns a self-describing envelope with a schema header, compact object rows using native JSON types, and built-in pagination support.
Request via HTTP:
Accept: application/vnd.fluree.agent+json
Fluree-Max-Bytes: 32768
Characteristics:
- Schema-once header: datatypes declared per variable, not repeated per value
- Native JSON types for values (strings, numbers, booleans — no wrappers for inferable types)
- Non-inferable datatypes annotated inline only where needed (
{"@value": ..., "@type": "..."}) - Byte-budget truncation with
hasMoreflag and resume query - Time-pinning metadata (
tfor single-ledger,isowallclock timestamp for cross-ledger)
Example (single-ledger, no truncation):
{
"schema": {
"?name": "xsd:string",
"?age": "xsd:integer",
"?s": "uri"
},
"rows": [
{"?name": "Alice", "?age": 30, "?s": "ex:alice"},
{"?name": "Bob", "?age": 25, "?s": "ex:bob"}
],
"rowCount": 2,
"t": 5,
"iso": "2026-03-26T14:30:00Z",
"hasMore": false
}
Example (truncated, with resume query):
{
"schema": {
"?name": "xsd:string",
"?age": "xsd:integer"
},
"rows": [
{"?name": "Alice", "?age": 30},
{"?name": "Bob", "?age": 25}
],
"rowCount": 2,
"t": 5,
"iso": "2026-03-26T14:30:00Z",
"hasMore": true,
"message": "Response truncated due to size limit of 32768 bytes. Use the query below to retrieve the next batch.",
"resume": "SELECT ?name ?age FROM <mydb:main@t:5> WHERE { ?s ex:name ?name ; ex:age ?age } OFFSET 2 LIMIT 100"
}
Schema types:
- Single type → string:
"?name": "xsd:string" - Mixed types → array:
"?value": ["xsd:string", "xsd:integer"] - IRI references →
"uri"
Envelope fields:
| Field | Present | Description |
|---|---|---|
schema | Always | Per-variable datatype map |
rows | Always | Array of {variable: value} objects |
rowCount | Always | Number of rows included |
t | Single-ledger only | Transaction number used for the query |
iso | Always | ISO-8601 wallclock timestamp at query time |
hasMore | Always | Whether more rows exist beyond the byte budget |
message | When truncated | Human-readable truncation explanation |
resume | When truncated, single-FROM only | Ready-to-execute SPARQL with @t: pinning and OFFSET |
Multi-ledger queries: The t field is omitted (each ledger has its own timeline). The resume field is also omitted; instead, the message instructs the caller to use @time: on each FROM clause for time-pinning.
Byte budget: Set via the Fluree-Max-Bytes header. When the cumulative serialized size of rows exceeds this limit, the formatter stops adding rows and sets hasMore: true. The budget applies to row data only (schema and envelope overhead are excluded from the count).
Graph Formats (CONSTRUCT / DESCRIBE)
A CONSTRUCT or DESCRIBE result is an RDF graph rather than a table of solutions, so it has its own formats:
| Format | HTTP Accept | Rust |
|---|---|---|
| JSON-LD (default) | application/ld+json, application/json, or none | FormatterConfig::jsonld() |
| Turtle | text/turtle | FormatterConfig::turtle() |
| N-Triples | application/n-triples | FormatterConfig::ntriples() |
| RDF/XML | application/rdf+xml | FormatterConfig::rdf_xml() |
| TriG | application/trig | FormatterConfig::trig() |
| N-Quads | application/n-quads | FormatterConfig::nquads() |
All but JSON-LD are strings: use execute_formatted_string() or format_results_string(). Every format sorts each graph and removes duplicate triples.
PREFIX ex: <http://example.org/ns/>
CONSTRUCT { ?p ex:label ?name } WHERE { ?p ex:name ?name }
As Turtle, the query’s PREFIXes (or a JSON-LD query’s @context prefixes) become @prefix declarations and prefixed names; each subject’s triples are grouped, rdf:type is written a, and integers, decimals, doubles and booleans are written bare:
@prefix ex: <http://example.org/ns/> .
ex:alice ex:label "Alice" .
ex:bob ex:label "Bob" .
N-Triples writes one triple per line with full IRIs. Both use the canonical N-Triples escapes for strings, and escape characters an IRI may not contain as \uXXXX, which reads back as the same IRI. Blank nodes keep their stored labels (_:fdb-…), so the output can be written back to the same nodes.
Named graphs. A CONSTRUCT template with GRAPH blocks produces a dataset (see CONSTRUCT). TriG writes the default graph as top-level Turtle and each named graph as a GRAPH <name> { ... } block; N-Quads adds the graph name as a fourth term; JSON-LD adds a {"@id": <name>, "@graph": [...]} node per named graph. Turtle, N-Triples and RDF/XML cannot express named graphs and refuse such a result (over HTTP, a 406). Without named graphs, TriG and N-Quads output are plain Turtle and N-Triples.
Edge annotations. A reifier attached to a result triple (see CONSTRUCT) is written as o ~ r after the object in Turtle and TriG, as an r rdf:reifies <<( s p o )>> line in N-Triples and N-Quads, as "@annotation": {"@id": r} on the value in JSON-LD, and as the RDF 1.2 rdf:annotation (or rdf:annotationNodeID) attribute on the property element in RDF/XML.
Array Normalization
By default, graph crawl results return single-valued properties as bare scalars and multi-valued properties as arrays:
{"schema:name": "Alice", "ex:tags": ["rust", "wasm"]}
This can be problematic for typed struct deserialization (e.g., a Vec<String> field that receives a bare string when only one value exists).
normalize_arrays forces all property values into arrays regardless of cardinality:
{"schema:name": ["Alice"], "ex:tags": ["rust", "wasm"]}
This is orthogonal to typed JSON and can be combined with any format:
#![allow(unused)]
fn main() {
// Typed + normalized — most predictable for struct deserialization
let config = FormatterConfig::typed_json().with_normalize_arrays();
// JSON-LD + normalized — compact values but predictable shapes
let config = FormatterConfig::jsonld().with_normalize_arrays();
}
The @container: @set context annotation still forces arrays per-property and works regardless of the normalize_arrays setting.
Format Selection
JSON-LD Query
JSON-LD Query defaults to JSON-LD format. You can specify the format explicitly:
{
"@context": { "ex": "http://example.org/ns/" },
"select": ["?name", "?age"],
"where": [
{ "@id": "?person", "ex:name": "?name", "ex:age": "?age" }
],
"format": "jsonld"
}
SPARQL
SPARQL queries return SPARQL JSON format by default:
PREFIX ex: <http://example.org/ns/>
SELECT ?name ?age
WHERE {
?person ex:name ?name .
?person ex:age ?age .
}
Datatype Handling
String Types
JSON-LD:
"Hello"
Typed JSON:
{"@value": "Hello", "@type": "xsd:string"}
SPARQL JSON:
{"type": "literal", "value": "Hello"}
Numeric Types
JSON-LD:
42
Typed JSON:
{"@value": 42, "@type": "xsd:long"}
SPARQL JSON:
{"type": "literal", "value": "42", "datatype": "http://www.w3.org/2001/XMLSchema#integer"}
Language-Tagged Strings
All formats use the same representation:
{"@value": "Hello", "@language": "en"}
IRIs
JSON-LD / Typed JSON:
{"@id": "ex:alice"}
SPARQL JSON:
{"type": "uri", "value": "http://example.org/ns/alice"}
Rust API
Use FormatterConfig to control output format via the query builder API:
#![allow(unused)]
fn main() {
use fluree_db_api::FormatterConfig;
// Single-ledger query with explicit format
let db = fluree.db("mydb:main").await?;
let result = db.query(&fluree)
.sparql("SELECT ?name WHERE { ?s <schema:name> ?name }")
.format(FormatterConfig::typed_json())
.execute_formatted()
.await?;
// Dataset query with format
let result = dataset.query(&fluree)
.sparql("SELECT * WHERE { ?s ?p ?o }")
.format(FormatterConfig::sparql_json())
.execute_formatted()
.await?;
// Connection-level query with format
let result = fluree.query_from()
.jsonld(&query_with_from)
.format(FormatterConfig::jsonld())
.execute_formatted()
.await?;
// AgentJson with byte budget and resume support
use fluree_db_api::AgentJsonContext;
let config = FormatterConfig::agent_json()
.with_max_bytes(32768)
.with_agent_json_context(AgentJsonContext {
sparql_text: Some(sparql.to_string()),
from_count: 1,
iso_timestamp: Some(chrono::Utc::now().to_rfc3339()),
});
let result = db.query(&fluree)
.sparql("SELECT ?name ?age WHERE { ?s ex:name ?name ; ex:age ?age }")
.format(config)
.execute_formatted()
.await?;
// Or directly on QueryResult:
let json = result.to_agent_json(&snapshot)?; // no budget
let json = result.to_agent_json_with_config(&snapshot, &config)?; // with budget
}
Available format constructors:
FormatterConfig::jsonld()— JSON-LD (default for JSON-LD queries)FormatterConfig::sparql_json()— SPARQL 1.1 JSON Results (default for SPARQL queries)FormatterConfig::typed_json()— Typed JSON with explicit datatypes on every valueFormatterConfig::agent_json()— Agent JSON envelope for LLM/agent consumersFormatterConfig::turtle()/ntriples()/rdf_xml()/trig()/nquads()— graph text formats for CONSTRUCT/DESCRIBE (see Graph Formats)
Builder methods:
.with_normalize_arrays()— Force array wrapping for all graph crawl properties.with_pretty()— Pretty-print JSON output.with_max_bytes(n)— Set byte budget for AgentJson truncation.with_agent_json_context(ctx)— Set SPARQL text, FROM count, and ISO timestamp for AgentJson resume queries
All three query paths (db.query(), dataset.query(), fluree.query_from()) support .format().
Direct formatting on QueryResult
For graph crawl queries (which require async DB access):
#![allow(unused)]
fn main() {
// Typed JSON with graph crawl support
let json = result.to_typed_json_async(db.as_graph_db_ref()).await?;
// Custom config (e.g., typed + normalize_arrays)
let config = FormatterConfig::typed_json().with_normalize_arrays();
let json = result.format_async(db.as_graph_db_ref(), &config).await?;
}
When no .format() is set:
- JSON-LD queries default to JSON-LD format
- SPARQL queries default to SPARQL JSON format
CLI Usage
The fluree query command supports format selection via --format:
# Default table output
fluree query "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5"
# JSON output
fluree query --format json '{"select": {"ex:alice": ["*"]}, "from": "mydb:main"}'
# Typed JSON output (explicit types on every value)
fluree query --format typed-json '{"select": {"ex:alice": ["*"]}, "from": "mydb:main"}'
# Normalize arrays (force all properties to arrays)
fluree query --format json --normalize-arrays '{"select": {"ex:alice": ["*"]}, "from": "mydb:main"}'
# Typed JSON + normalize arrays (most predictable for programmatic use)
fluree query --format typed-json --normalize-arrays '{"select": {"ex:alice": ["*"]}, "from": "mydb:main"}'
Performance Considerations
- JSON string output is streamed — when emitting a JSON string (the
format_results_string*entry points used by the HTTP server), JSON-LD, SPARQL JSON, Typed JSON, and Agent JSON serialize directly into the output buffer, skipping the intermediateserde_json::ValueDOM and its second serialization pass. Output is byte-identical to the DOM path (enforced by parity tests). The DOM path (serde_json::Value-returningformat_results*) is still used for the programmaticValueAPI, forprettyoutput, and as the parity reference;selectOne, ASK, CONSTRUCT/DESCRIBE, and hydration also fall back to it. - JSON-LD is the most efficient format — inferable types skip the
@value/@typewrapper - Typed JSON adds a constant-factor overhead per literal value (the
@value/@typewrapper). Query execution is unaffected — only the formatting phase is slower. - normalize_arrays adds zero overhead when disabled (default). When enabled, it skips the
len() == 1check — no additional allocations beyond the array wrapper. - TSV/CSV bypass JSON construction entirely for maximum throughput
- Turtle, N-Triples, RDF/XML, TriG and N-Quads write the constructed graph straight into one output string, with no JSON DOM; decoded IRIs are shared across the triples that repeat them. They allocate far less than CONSTRUCT’s JSON-LD, which builds a
serde_json::ValueDOM - Graph output is not streamed or paged. Every graph format builds the whole constructed graph, then the whole document, before the first byte is sent, so memory grows with the result. For a large graph, bound the CONSTRUCT with
LIMIT/OFFSET, or export the ledger.
Best Practices
- Use JSON-LD for human-facing apps: Compact and readable
- Use Typed JSON for struct deserialization: Unambiguous types prevent parsing surprises
- Use
normalize_arraysfor typed consumers: EnsuresVec<T>fields always get arrays - Use SPARQL JSON for standard tooling: Interoperable with SPARQL clients
- Use TSV/CSV for bulk export: Highest throughput, smallest memory footprint
- Use Agent JSON for LLM/agent integrations: Schema-once + pagination prevents context window overflow
Related Documentation
- JSON-LD Query: JSON-LD Query language
- SPARQL: SPARQL query language
- Datatypes: Type system details