Intro to JSON-LD: JSON for the semantic web

JSON-LD extends JSON to include linking information. Here's how it could make JSON a top-tier player in hypermedia and the semantic web.

JSON-LD extends JSON to include linking information.

JSON-LD looks to unite the promise of self-describing hypermedia with the simple familiarity of JSON. By incorporating data links, JSON-LD lets developers add semantic information to a typical JSON API. Let's take a look at this powerful extension to JSON.

What is JSON-LD?

JSON-LD (JavaScript Object Notation for Linked Data) extends the JSON format to include linking information. The extension has a wide range of uses from knowledge graphs to SEO. One of the most interesting use cases for JSON-LD is bringing JSON-based APIs closer to the original vision for REST, where hypermedia documents are self-describing and maintain loose coupling between server and client. 

A big problem with typical JSON APIs is that they create a strong coupling between the client and the server. The client knows where assets live and their relationships in the services. Hard-coding the URLs, as JSON does, creates a point of brittleness in clients. It contradicts the ideal of REST, wherein the assets themselves tell the client where to find the related information.

At a high level, JSON-LD makes JSON a member of the semantic webaka Web 3.0. The semantic web is an idea for how to make the web more meaningful. It looks to replace the current system of arbitrary locations with an interconnected network of related assets. This is a big project with many facets. For our discussion here, we'll focus on the hands-on experience of working with JSON-LD, by building an application that demonstrates the semantic web concept in a client-server setup.

JSON-LD in a client-server application

To get a sense for how JSON-LD works, we'll build a small client-server application that serves JSON-LD from a Node.js server, along with a simple UI that lets us navigate the structure. We’ll use an interrelated set of data describing music. We’ll be able to start with a list of albums—just a couple to keep things simple—then click on an album name to see simple data about it. We'll also be able to click on a song name and see who the songwriters were.

Let's start by displaying our albums by name and letting the user click on them for more information. Instead of hardcoding the URL where we've stored the album details, we'll use JSON-LD to define the URL in the JSON itself, as the ID. This setup moves us away from using a number for the ID and toward using the actual location.

Listing 1. Simple JSON-LD endpoint (server.js)


const express = require('express');
const app = express();

const albumsData = [
  {
    "@id": "/music/albums/1",
    "name": "Abbey Road",
    "songs": [
      {
        "@id": "/music/songs/1",
        "name": "Here Comes the Sun",
        "songwriters": ["George Harrison"]
      }
    ]
  },
  {
    "@id": "/music/albums/2",
    "name": "Let It Be",
    "songs": [
      {
        "@id": "/music/songs/2",
        "name": "Let It Be",
        "songwriters": ["John Lennon", "Paul McCartney"]
      }
    ]
  }
];

// Define a route to serve album data
app.get('/music/albums', (req, res) => {
  res.json(albumsData);
});

app.get('/music/albums/:albumId', (req, res) => {
  const albumId = req.params.albumId;
  const album = albumsData.find(album => album["@id"] === `/music/albums/${albumId}`);

  if (album) {
    res.json(album);
  } else {
    res.status(404).json({ error: "Album not found" });
  }
});

app.use(express.static('public'));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

The data that the endpoint issues is typical JSON, except that it has the @id property. Properties beginning with the address sign (@ symbol) are JSON-LD-specific. In this case, we provide an actual link that the client can use. Note that in the Web 3.0 ideal, links would be absolute, to help form a universally navigable knowledge graph. We are using the relative URLs for development purposes.

In a typical JSON API, we’d have an ID field and an integer for a value. Then, the client would form a URL based on that ID. In this case, the client can actually use the ID field as-is. The server handles such URLs as appropriate in the /music/albums/:albumId endpoint. (A real-world endpoint would require more sophisticated handling.)

A simple client for this API would look like Listing 2.

Listing 2. Simple JSON-LD client (public/index.html)


<!DOCTYPE html>
<html>
<head>
    <title>JSON-LD Client</title>
</head>
<body>
  <h1>Albums</h1>
  <ul id="album-list"></ul>
  <div id="album-details"></div>

  <script>
    const albumList = document.getElementById("album-list");
    const albumDetails = document.getElementById("album-details");

    // Function to fetch and display albums
    function fetchAlbums() {
      fetch("/music/albums")
        .then(response => response.json())
        .then(albums => {
           albums.forEach(album => {
             const listItem = document.createElement("li");
             listItem.innerHTML = `<a href="#" data-album-id="${album["@id"]}">${album.name}</a>`;
             albumList.appendChild(listItem);
           });
         });
       }

       // Function to fetch and display album details
       function fetchAlbumDetails(albumId) {
         fetch(albumId)
           .then(response => response.json())
           .then(album => {
              const albumInfo = `
                <h2>${album.name}</h2>
                <h3>Songs</h3>
                <ul>
                  ${album.songs.map(song => `<li><a href="#" data-song-id="${song["@id"]}">${song.name}</a></li>`).join("")}
                </ul>
              `;
            albumDetails.innerHTML = albumInfo;
          });
        }

        // Event listener for album links
        albumList.addEventListener("click", (event) => {
          if (event.target.tagName === "A") {
            const albumId = event.target.getAttribute("data-album-id");
            fetchAlbumDetails(albumId);
          }
        });

        // Event listener for song links
        albumDetails.addEventListener("click", (event) => {
          if (event.target.tagName === "A") {
            const songId = event.target.getAttribute("data-song-id");
            alert("You clicked on song with ID: " + songId);
          }
        });

        // Initialize the client by fetching and displaying albums
        fetchAlbums();
    </script>
</body>
</html>

Listing 2 is a greatly simplified example, designed to highlight the main point: that the client can deal with the links in a generic fashion, by simply issuing fetch requests against the @id fields. This is a first step toward generalizing clients so that they avoid knowing about the structure of the service API.

The @Context and @Type fields

There is a lot more to JSON-LD, and the @context and @type fields are a big part of how it helps form semantic documents. There are several public repositories for context and type information that you can use, including schema.org, XML namespaces, and JSON-LD’s contexts

Schema.org is a consortium of search engines and is vast. XML Namespaces are ancient and familiar, and also more extensive. JSON-LD’s contexts are intended to be simpler and still comprehensive. An application can also define its own types, and use a blend of definition sources.

The basic idea to the @context and @type fields is to let the JSON document describe what its content is in a (more) universally discoverable format.

  • The @context property defines the namespace for the document. It acts as a mapping between terms used in the document and their definitions, making the data's meaning clearer to humans and machines.
  • The @type property lets us define the type for the entities or data elements. This type helps categorize the data, making it easier to interpret.

In Listing 3, we see how to use the schema.org @context and definitions like the MusicComposition @type.

Listing 3. Using @context and @type


const albumsData = [
  {
    "@context": {
      "music": "http://schema.org/",
    },
    "@type": "music:MusicAlbum",
    "@id": "/music/albums/1",
    "name": "Abbey Road",
    "songs": [
      {
        "@type": "music:MusicComposition",
        "@id": "/music/songs/1",
        "name": "Here Comes the Sun",
        "songwriters": [
          {
            "@type": "music:MusicGroup",
            "name": "The Beatles",
          }
        ]
      }
    ]
  },
  {
    "@context": {
      "music": "http://schema.org/",
    },
    "@type": "music:MusicAlbum",
    "@id": "/music/albums/2",
    "name": "Let It Be",
    "songs": [
      {
        "@type": "music:MusicComposition",
        "@id": "/music/songs/2",
        "name": "Let It Be",
        "songwriters": [
          {
            "@type": "music:MusicGroup",
            "name": "The Beatles",
          }
        ]
      }
    ]
  }
];

We've used the definition hosted at schema.org and given it the name “music.” The definition acts as a namespace when we give types to our data elements; in this case, music:MusicComposition and music:MusicGroup.

The client can now use the @context and @type data. For a simple example, in our application, we could make decisions about how to display the data based on these fields. Note that the application isn’t actually fetching anything from schema.org; it’s only using it as a unique namespace and as a reference for structuring its own data.

Expand and compact

In reference to @type, it’s worth mentioning the notion of expand and compact. Expanding allows for a more verbose view, while compact makes for a tighter syntax. Expand will take the @type, like our music:MusicAlbum, and expand it to http://schema.org/MusicAlbum. Compact will do the reverse.

Libraries like jsonls.js support these operations, and others, such as converting to RDF format.

Conclusion

JSON-LD will play an important part in making semantic web ideals more realizable for developers. JSON is the lingua franca of API formats, and JSON-LD provides an easy entry point for adding semantic metadata to it. 

Copyright © 2023 IDG Communications, Inc.