---
title: JSON-LD framing
url: https://docs.triply.cc/tutorials/json-ld-framing/
description: Turn the output of a SPARQL Construct query into predictable, tree-shaped JSON with a JSON-LD
  frame, and serve it from a saved query.
---

# JSON-LD framing

A graph has no beginning. A REST API response does. This tutorial closes that gap: you take a
SPARQL `construct` query and turn its output into predictable, tree-shaped JSON that any
application can consume — without writing a service in between.

It assumes [SPARQL: the basics](https://docs.triply.cc/concepts/sparql-basics/) and
[construct queries](https://docs.triply.cc/concepts/sparql-construct-and-federation/).

## Why framing is needed

SPARQL Construct and SPARQL Describe queries can return results in the JSON-LD format. Here is an example:

```json
[
  {
    "@id": "john",
    "livesIn": { "@id": "amsterdam" }
  },
  {
    "@id": "jane",
    "livesIn": { "@id": "berlin" }
  },
  {
    "@id": "tim",
    "livesIn": { "@id": "berlin" }
  }
]
```

JSON-LD is one of the serialization formats for RDF, and encodes a graph structure. For example, the JSON-LD snippet above encodes the following graph:

```mermaid
graph TB
  Tim -- livesIn --> Berlin
  John -- livesIn --> Amsterdam
  Jane -- livesIn --> Berlin
```

The triples in a graph do not have any specific order. In the picture above, the triple about Tim
is mentioned first, but that is arbitrary: a graph is a set of triples, so there is no "first" or
"last" one. There is no "primary" or "secondary" element either. Persons happen to be drawn on
the left and cities on the right, but exactly the same information is expressed by this graph:

```mermaid
graph TB
  Berlin -- hasInhabitant --> Tim
  Berlin -- hasInhabitant --> Jane
  Amsterdam -- hasInhabitant --> John
```

Most RESTful APIs, on the other hand, return data with a specific, often tree-shaped structure. For example:

```json
{
  "amsterdam": {
    "inhabitants": [
      "john"
    ]
  },
  "berlin": {
    "inhabitants": [
      "jane",
      "tim"
    ]
  }
}
```

**JSON-LD framing** is the standard that gets you from the first to the second. A frame is a
deterministic translation from a graph — an unordered set of triples, with no node that is first
or special — into a tree, with ordered branches and exactly one root. It is what makes it
possible to serve a SPARQL query as a REST API.

You need two things: a `construct` query and a frame. TriplyDB's saved query API carries a
JSON-LD profiler that applies the frame to the query result and returns plain JSON.

## Before you start

You need a dataset you can save queries on — the one from
[Get started with TriplyDB](https://docs.triply.cc/tutorials/getting-started/) will do — and an API token for Step 4. See
[Create an API token](https://docs.triply.cc/how-to/create-an-api-token/).

## Step 1: Write the construct query

The query has to produce a graph that stands on its own: self-contained, and carrying the
vocabulary and data the response needs. This one goes further and carries its data in the
`construct` template itself, under an empty `where` clause, so it returns the same 18 triples
whichever dataset it is saved on: two objects, a head office with two addresses and a depot with
one, each labelled in English and Dutch.

```sparql
prefix ex: <https://triply.cc/example/>
construct {
  ex:office a ex:Object;
    ex:label "Head office"@en, "Hoofdkantoor"@nl;
    ex:address ex:address-1, ex:address-2.
  ex:depot a ex:Object;
    ex:label "Depot"@en, "Depot"@nl;
    ex:address ex:address-3.
  ex:address-1 a ex:Address; ex:street "Keizersgracht"; ex:number 391.
  ex:address-2 a ex:Address; ex:street "Prinsengracht"; ex:number 263.
  ex:address-3 a ex:Address; ex:street "Westerdoksdijk"; ex:number 40.
}
where {}
```

Open your dataset, click **SPARQL** in the left-hand sidebar, paste the query in and click the
save button. Name the query `json-ld-frame`. Saving gives it an API URL, starting with `api` and
ending in `run`, which the query page shows next to the keyword `API`:

```iri
https://api.triplydb.com/queries/ACCOUNT/json-ld-frame/run
```

`ACCOUNT` is the account that owns the dataset, and the host is your own instance's API host if
you are not on triplydb.com.

A query over real data has a `where` clause that matches it, and can take API variables, appended
to the URL as `?[queryVariable]=[value]`. Three behaviours of such queries catch people out, and
each is easier to recognise than to debug:

- **`optional` and API variables interact badly.** An API variable used inside an `optional` does
  not filter the results, so the query returns false positives.
- **`union` can split the result set.** The engine exhausts the first branch before starting the
  second, so the two halves may be disconnected. With a small limit, the result ends up split
  across two JSON-LD documents and data goes missing from the response.
- **A page may hold fewer results than `pageSize`.** The limit applies to the `where` clause, not
  the `construct` clause, so two rows of the `where` result can condense into a single result.
  The response size can differ from the page size even when the next page is not empty.

## Step 2: Write the frame

A SPARQL query alone cannot express the syntactic shape a JSON consumer needs, which is what the
frame adds. A frame has two parts: a `@context` and a structure. The full specification is at
[w3c.github.io/json-ld-framing](https://w3c.github.io/json-ld-framing/).

**The `@context`** maps linked data to JSON names. Each key is the name the IRI will take in the
response; each value is the IRI itself. Usually that is a one-to-one mapping to a string. When
the value is an object it must contain at least `@id`, and may carry modifiers — `@type` to set
the datatype of the value, or `@container` to say what kind of container it lands in. The context
can also hold prefixes and vocabulary references.

**The structure** defines the shape of the response. It normally starts with `@type`, which is
the most direct way to choose the root node, and is built outwards from there. An empty pair of
braces marks a leaf. To nest a node, name the property that points to another IRI, then describe
that node's properties inside it.

```json
{
  "@context": {
    "addresses": "ex:address",
    "Address": "ex:Address",
    "Object": "ex:Object",
    "street": "ex:street",
    "number": {
      "@id": "ex:number",
      "@type": "xsd:integer"
    },
    "labels": {
      "@id": "ex:label",
      "@container": "@set"
    },
    "ex": "https://triply.cc/example/",
    "xsd": "http://www.w3.org/2001/XMLSchema#"
  },
  "@type": "Object",
  "labels": {},
  "addresses": {
    "street": {},
    "number": {}
  }
}
```

## Step 3: Try the frame on the query page

Framing applies to the RDF that a CONSTRUCT query returns, so the LD-Frame editor is offered on
the saved query's own page, not in the SPARQL IDE. Open the saved query, run it, and choose
"LDFrame" from the list of visualizations.

![The list of visualizations on a saved query](https://docs.triply.cc/assets/json-ld-navigator.png)

Paste the frame from Step 2 into the JSON-LD Frame editor. The framed output appears below it,
under "Framed query results":

![The LD-Frame editor with the frame pasted in](https://docs.triply.cc/assets/json-ld-script.png)

Scroll through the results. Each object is now a node of its own, with its addresses nested
inside it:

![The framed query results](https://docs.triply.cc/assets/json-ld-result.png)

## Step 4: Call the API

Send the same frame as the body of a request to the saved query's API URL. Replace `ACCOUNT` with
your account name and `TOKEN` with your API token:

```sh
curl -X POST https://api.triplydb.com/queries/ACCOUNT/json-ld-frame/run \
  -H 'Accept: application/ld+json;profile=http://www.w3.org/ns/json-ld#framed' \
  -H 'Content-type: application/json' \
  -H 'Authorization: Bearer TOKEN' \
  -d '{
    "@context": {
      "addresses": "ex:address",
      "Address": "ex:Address",
      "Object": "ex:Object",
      "street": "ex:street",
      "number": {
        "@id": "ex:number",
        "@type": "xsd:integer"
      },
      "labels": {
        "@id": "ex:label",
        "@container": "@set"
      },
      "ex": "https://triply.cc/example/",
      "xsd": "http://www.w3.org/2001/XMLSchema#"
    },
    "@type": "Object",
    "labels": {},
    "addresses": {
      "street": {},
      "number": {}
    }
  }'
```

Four details matter, and each one produces a different failure when it is wrong:

- It must be a **`POST`**. Only a `POST` carries a frame in its body.
- The **`Accept`** header needs both the content type and the framing profile:
  `application/ld+json;profile=http://www.w3.org/ns/json-ld#framed`.
- The **`Content-type`** describes the body, which is the frame, so it is `application/json`.
- The **`Authorization: Bearer [TOKEN]`** header is required for internal or private queries.
  See [Create an API token](https://docs.triply.cc/how-to/create-an-api-token/).

The response is the frame's `@context` followed by an `@graph` holding one object per
`ex:Object`:

```json
{
  "@context": { … },
  "@graph": [
    {
      "@id": "ex:depot",
      "@type": "Object",
      "addresses": {
        "@id": "ex:address-3",
        "@type": "Address",
        "number": "40",
        "street": "Westerdoksdijk"
      },
      "labels": [
        { "@language": "en", "@value": "Depot" },
        { "@language": "nl", "@value": "Depot" }
      ]
    },
    {
      "@id": "ex:office",
      "@type": "Object",
      "addresses": [
        {
          "@id": "ex:address-1",
          "@type": "Address",
          "number": "391",
          "street": "Keizersgracht"
        },
        {
          "@id": "ex:address-2",
          "@type": "Address",
          "number": "263",
          "street": "Prinsengracht"
        }
      ],
      "labels": [
        { "@language": "en", "@value": "Head office" },
        { "@language": "nl", "@value": "Hoofdkantoor" }
      ]
    }
  ]
}
```

Notice the difference between `labels` and `addresses`. `labels` is declared with
`"@container": "@set"`, so it is an array even where there is one value. `addresses` is not, so
the depot's single address comes back as an object and the office's two as an array — the kind of
irregularity a client has to code around, and which one more `@container` in the context removes.

## What you now have

A SPARQL query that behaves like a REST endpoint: a stable URL, plain JSON out, and a shape that
will not change because the underlying triples came back in a different order.

## Related

- [Build a RESTful API](https://docs.triply.cc/tutorials/building-a-restful-api/) — the same technique, applied end to end
- [SPARQL: construct and federation](https://docs.triply.cc/concepts/sparql-construct-and-federation/) — the query form this
  depends on
- [Work with saved queries](https://docs.triply.cc/how-to/saved-queries/) — saving the query the frame runs against

<!-- EDITORIAL NOTE, not for readers: the second diagram above was missing from the published
     page entirely ("the same information can be expressed with the following graph:" followed by
     nothing); the inverse predicate is called hasInhabitant here, which the source never named,
     so change it if there is a house preference. The first diagram also placed Jane in Amsterdam
     while the JSON above it placed her in Berlin; it now says Berlin, matching the JSON and the
     tree further down. The query in Step 1 carries the same 18 statements as the seeded
     Documentation/json-ld-frame-example dataset on demo.triplydb.com, so the Step 3 screenshots
     show exactly what the reader gets. -->
