Skip to Content

2022.04.21

JSON-LD is good, actually.

In which we try and understand what JSON-LD is all about, what it's good for, and why it's interesting and cool.

What is JSON-LD

  • JSON-LD is a format for expressing Linked Data.
  • Linked Data is a way of creating graph data structures.
  • Graph data structures are about relations.

What does JSON-LD Do?

  • JSON-LD separates the data from the format. Weird.
  • JSON-LD contextualizes the JSON object and its’ data.
  • JSON-LD removes ambiguity of keys.

Removing Ambiguity

Consider the following JSON object:

{
  id: 0x01,
  name: "Widget"
}

What does that mean? How do we use this object? Removed from the context of the application and/or service documentation we don’t know what this object is or what these keys are for. We can make some assumptions, but we can’t be sure.

We can turn this into JSON-LD by adding a @context.

@context is a key/value object that associates ambiguous key names with unambiguous URIs. Like so:

// @context object
{
  id: @id,
  name: uri:uuid:thing-name
}

This context object says “When we say id we mean a node identifier. When we say name we mean this exact unique thing right here.`

We use our context to make JSON-LD:

{
  @context: {
    id: @id,
    name: uri:uuid:thing-name
  },
  id: 0x01,
  name: "Widget"
}

This can then be expanded into an object that is unambiguous, albeit gross to work with:

const object = {
  "@context": {
    id: "@id",
    name: "uri:uuid:thing-name"
  },
  id: "0x01",
  name: "Widget"
}

const expand = async () => {
  let expanded = await jsonld.expand(object)
  let terser = await jsonld.compact(expanded, {})
  return terser
}

return await expand()
…

But as long as we have our context object, we can easily turn it back into a nice, compressed JSON object that doesn’t involve using URIs as keys:

const context = {
  id: "@id",
  name: "uri:uuid:thing-name"
}

const doc = [
  {
    "@id": "0x01",
    "uri:uuid:thing-name": [
      {
        "@value": "Widget"
      }
    ]
  }
]

const compact = async () => {
  return await jsonld.compact(doc, context)
}

return await compact()
…

Contextualize the Object and it’s keys

Removing ambiguity is neat. It relies on a a given context, which implies that we can provide different contexts and get different absolute objects.

What if we want to identify our object on it’s name?

const object = {
  "@context": {
    id: "uri:uuid:some-uuid",
    name: "@id"
  },
  id: "0x01",
  name: "Widget"
}

const contextualize = async () => {
  let expanded = await jsonld.expand(object)
  let terser = await jsonld.compact(expanded, {})
  return terser
}

return await contextualize()
…

This transformation means that object keys are arbitrary. They only are useful in terms of what context they are being operated on within.

Lets say we have two different objects from two different data sources:

// Thing One
{
  id: 0x01,
  name: "Widget"
}
// Thing Two
{
  uri: "thing-two-slug",
  title: "Fidget"
}

We can harmonize them by providing each object contextual information:

// Context One
{
  id: @id,
  name: uri:uuid:thing-name
}
// Context Two
{
  uri: @id,
  title: uri:uuid:thing-name
}

We can turn both objects into absolute objects:

const context = {
  id: "@id",
  name: "uri:uuid:thing-name",
  uri: "@id",
  title: "uri:uuid:thing-name"
}

const objects = {
  "@context": context,
  "@graph": [
    {
      id: "0x01",
      name: "Widget"
    },{
      uri: "thing-two-slug",
      title: "Fidget"
    }
  ]
}

const harmonize = async () => {
  let expanded = await jsonld.expand(objects)
  let terser = await jsonld.compact(expanded, {})
  return terser
}

return await harmonize()
…

Or transform both objects into either context:

const contextOne = {
  id: "@id",
  name: "uri:uuid:thing-name",
}

const contextTwo = {
  uri: "@id",
  title: "uri:uuid:thing-name",
}

const objects = [
  {
    "@id": "0x01",
    "uri:uuid:thing-name": [
      {
        "@value": "Widget"
      }
    ]
  },
  {
    "uri:uuid:thing-name": [
      {
        "@value": "Fidget"
      }
    ],
    "@id": "thing-two-slug"
  }
]

const compact = async () => {
  // uncomment to
  // translate into
  // context one.
  // return await jsonld.compact(objects, contextOne)
  return await jsonld.compact(objects, contextTwo)
}

return await compact()
…

@graph

Note the introduction of the @graph key – this is how we gather multiple objects together into a single, contextualized object. Every object in the @graph array is a Subject in the data.

Separate the data from the format

JSON keys are fundamentally arbitrary. As is the structure of the JSON. This is why we can use GraphQL to request JSON blobs of different shapes and nested objects.

JSON-LD gives us a way to take the underlying data being represented by an object and decouple it from the JSON format itself.

It does this by converting a single object with key/value pairs into a number of tuples, in this case 3-tuples (also knows as “triples” for normal non math humans).

The two objects above, in Context One, look like this when represented as triples:

const objects = {
  "@context": {
    "@base": "uri:",
    uri: "@id",
    thingName: "uri:uuid:thing-name",
  },
  "@graph": [{
    uri: "0x01",
    thingName: "Widget"
  },{
    uri: "thing-two-slug",
    thingName: "Fidget"
  }]
}

const triplify = async () => {
  return await jsonld.toRDF(objects, {format: 'application/n-quads'})
}

return await triplify()
…

When we make transformations on JSON-LD objects we are storing the underlying data in-memory as triples, which are very fast and easy for code to work with. We then request transformations and particular shapes of this data to use in our application.

@base

We’ve introduced another keyword here — @base. One of the core principles of JSON-LD is that every node or object needs it’s own URI as a way to identify it. A string like thing-two-slug isn’t a URI, but if we append it to a URI protocol is can be: uri:thing-two-slug. More commonly, this is a URL like https://hefty.mart, which would give is the URI https://hefty.mart/thing-two-slug.

Data with Relationships

Let’s say our two things are manufactured by the same company, HeftyMart™.

{
  url: https://hefty.mart
  name: "HeftyMart™"
  location: "The County"
}

Lets contextualize this object:

{
  @context: {
    url: @id,
    name: uri:uuid:company-name,
    location: uri:uuid:physical-place
  },
  url: https://hefty.mart
  name: "HeftyMart™"
  location: "The County"
}

Note how “name” in this context means something different then in our previous contexts. We are making a statement that “company names” and different sorts of things than “thing names”.

Now we have three objects, each contextualized. As before, we can harmonize these objects into a single JSON-LD object, and convert the resulting object into the underlying data as triples:

const graph = {
  "@context": {
    "@base": "uri:",
    uri: "@id",
    companyName: "uri:uuid:company-name",
    thingName: "uri:uuid:thing-name",
    location: "uri:uuid:physical-place"
  },
  "@graph": [{
    uri: "https://hefty.mart",
    companyName: "HeftyMart™",
    location: "The County"
  },{
    uri: "/0x01",
    thingName: "Widget"
  },{
    uri: "/thing-two-slug",
    thingName: "Fidget"
  }]
}

const triplify = async () => {
  return await jsonld.toRDF(graph, {format: 'application/n-quads'})
}

return await triplify()
…

Making connections

Graphs are mathematical structures composed of “nodes” (which we can think of as Objects in this case) and “edges” (which we can think of as relationships).

We have three nodes in this graph. Can we add relationships?

{
  @context: {
    …
    madeBy: {
      "@id": "uri:uuid:thing-is-made-by",
      "@type": "@id"
    }
    …
  },
  @graph: […]
}

This defines a key as representing a relationship. It says “The madeBy key is identified by this URI. Its value is another node’s URI.”

We can use the relationship like so:

{
  @context: {…}
  @graph: [{
    uri: https://hefty.mart,
    companyName: "HeftyMart™",
    location: "The County"
  },{
    uri: 0x01,
    thingName: "Widget",
    madeBy: https://hefty.mart
  },{
    uri: thing-two-slug,
    thingName: "Fidget",
    madeBy: https://hefty.mart
  }]
}

Now our in-memory triples look like this:

const graph = {
  "@context": {
    "@base": "uri:",
    uri: "@id",
    companyName: "uri:uuid:company-name",
    thingName: "uri:uuid:thing-name",
    location: "uri:uuid:physical-place",
    madeBy: {
      "@id": "uri:uuid:thing-is-made-by",
      "@type": "@id"
    }
  },
  "@graph": [{
    uri: "https://hefty.mart",
    companyName: "HeftyMart™",
    location: "The County"
  },{
    uri: "/0x01",
    thingName: "Widget",
    madeBy: "https://hefty.mart"
  },{
    uri: "/thing-two-slug",
    thingName: "Fidget",
    madeBy: "https://hefty.mart"
  }]
}

const triplify = async () => {
  return await jsonld.toRDF(graph, {format: 'application/n-quads'})
}

return await triplify()
…

Frames of reference

There’s lots of ways that we can reference this data as different sorts of JSON blobs:

// a company-first frame of reference
{
  companyName: "HeftyMart™",
  makesThings: [{
    thingName: "Widget",
  }, {
    thingName: "Fidget",
  }]
}

Or, if we like:

// a thing-first frame of reference
{
  things: [{
    name: "Widget",
    madeBy: {
      companyName: "HeftyMart™"
    }
  }, {
    name: "Fidget",
    madeBy: {
      companyName: "HeftyMart™"
    }
  }]
}

The JSON becomes an arbitrary structure that can contain our data in any shape we need for our application.

Defining Frames

With JSON-LD, we can define the frame we want our resulting object to be in, much like a GraphQL query:

{
  @context: {…},
  "madeBy": {
    "@explicit": true,
    "companyName": {}
  }
}
const context = {
  "@base": "uri:",
  uri: "@id",
  companyName: "uri:uuid:company-name",
  thingName: "uri:uuid:thing-name",
  location: "uri:uuid:physical-place",
  madeBy: {
    "@id": "uri:uuid:thing-is-made-by",
    "@type": "@id"
  }
}

const graph = {
  "@context": context,
  "@graph": [{
    uri: "https://hefty.mart",
    companyName: "HeftyMart™",
    location: "The County"
  },{
    uri: "/0x01",
    thingName: "Widget",
    madeBy: "https://hefty.mart"
  },{
    uri: "/thing-two-slug",
    thingName: "Fidget",
    madeBy: "https://hefty.mart"
  }]
}

const frame = {
  "@context": context,
  "madeBy": {
    "@explicit": true,
    "companyName": {}
  }
}

const framed = async () => {
  return await jsonld.frame(graph, frame)
}

return await framed()
…

Or we can define the opposite, and grab the company by it’s uri with an array of things it makes:

{
  @context: {
    …
    makesThings: {
      "@reverse": "madeBy"
    }
  },
  "uri": "https://hefty.mart",
  "madeBy": {
    "@explicit": true,
    "companyName": {}
  }
}
const context = {
  "@base": "uri:",
  uri: "@id",
  companyName: "uri:uuid:company-name",
  thingName: "uri:uuid:thing-name",
  location: "uri:uuid:physical-place",
  makesThings: {"@reverse": "madeBy"},
  madeBy: {
    "@id": "uri:uuid:thing-is-made-by",
    "@type": "@id"
  }
}

const graph = {
  "@context": context,
  "@graph": [{
    uri: "https://hefty.mart",
    companyName: "HeftyMart™",
    location: "The County"
  },{
    uri: "/0x01",
    thingName: "Widget",
    madeBy: "https://hefty.mart"
  },{
    uri: "/thing-two-slug",
    thingName: "Fidget",
    madeBy: "https://hefty.mart"
  }]
}

const frame = {
  "@context": context,
  "uri": "https://hefty.mart",
  "makesThings": {
    "@explicit": true,
    "thingName": {}
  }
}

const framed = async () => {
  return await jsonld.frame(graph, frame)
}

return await framed()
…

@reverse

Another keyword! This one is for defining relationships between keys. By declaring in our context that makesThings is the reverse of madeBy, we are setting up a two-way relationship that allows is to list all the nodes with madeBy: "https://hefty.mart" under HeftyMarts makesThings key. Since the underlying data is strucutred as a triple, we’re really just writing a rule that informs another way we can convert that triple into a key-value pair on an object.

More about frames

The frames spec is suprisingly readable, and goes into full detail about what’s possible with JSON-LD frames.

JSON-LD in the wild

To put JSON-LD on a webpage, all you have to do is stick your blob into a script tag with a type declaration:

<script type="application/ld+json">
  {
    … blob goes here!
  }
</script>

You can have as many of these blobs on the page as you want! For example, this How-To article on the Verge has two. One contains data about the article, and the other contains specific data on the How-To structure of the article.

Tools like Schema.orgs validator can fetch the markup of the page, extract the JSON-LD blobs, and then operate on the data.

This is how Google search uses JSON-LD for improving search results and SEO – by adding that data to our pages we are making visible information in the from of triples, ie:

<https://www.theverge.com/21495830/android-11-multitasking-pane-recent-apps-screenshots-google-how-to> <https://schema.org/Type> <https://schema.org/How-To> .

This seperates the content of our data from our own structure, and allows Google to mesh it into their own knowledge graph systems.

To Sum Up

JSON-LD is a powerful tool for manipulating data – it lets us harmonize data from different sources, transform the shape of that data, and allows for an easy interface for making our data interoperable with other systems.

Elsewhere

json-ld

© 2026