An Introduction to JSON-LD
Tags [ JSON-LD ]
I’ve been spending some time recently thinking about ways to maintain loose coupling between lots of different teams. I suspect this involves thinking about shared protocols between the teams (i.e. having well-defined interactions) more than worrying about internal “implementations” like what flavor of Agile you use or what programming language you write in.
The World Wide Web is a pretty obvious example of a system that did
this well, and its architectural style was described in
Roy Fielding’s thesis.
I’m not going to refer to this style by its well-known
acronym, because I’ve done a lot of writing about it in the past, and
that’s not really the point. For this article, I just want to
highlight that one of the key principles of the Web’s architecture is
that of self-descriptive messages (which is, in turn, part of the
uniform interface architectural constraint). In other words, a
message should carry enough information that you can understand what
it means and what to do with it. For example, the HTTP response
containing the words you’re reading now came marked with a header
declaring Content-Type: text/html
, which in turn tells your browser
how to process the stream of bytes: by parsing and rendering the
markup.
Now, many APIs today use JSON as a message format. It is simple, has
parsers available for many programming languages, and is easy for
humans to read and write. However, JSON only gets us part of the
way to what I really would think of as self-describing messages. If an
API response is marked as Content-Type: application/json
, it tells
me how to parse it, but not what I can do with it or what it
means. Even this web page has more semantics available than JSON, as
your browser at least knows to render <a>
tags as clickable links,
even if it doesn’t know what the links mean, or what the content on
this page is about.
Actually, though, that’s not quite true. The <head>
of this page
includes some metadata that allows programmatic clients to make some
sense of the page content:
<head>
<meta charset="utf-8" />
<meta name="description" content="..." />
<meta name="author" content="Jon Moore" />
<meta property="og:title" content="An Introduction to JSON-LD" />
<meta property="og:description" content="..." />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://blog.jonm.dev/posts/intro-json-ld/" />
<meta property="article:published_time" content="2019-11-30T10:53:43-05:00" />
<meta property="article:modified_time" content="2019-11-30T10:53:43-05:00" />
<meta itemprop="name" content="An Introduction to JSON-LD">
<meta itemprop="description" content="...">
<meta itemprop="datePublished" content="2010-08-11T21:34:00-05:00" />
<meta itemprop="dateModified" content="2010-08-11T21:34:00-05:00" />
<meta itemprop="wordCount" content="...">
<meta itemprop="keywords" content="architecture,hypermedia,REST,REST API,RESTful web services,XHTML," />
<meta name="twitter:card" content="summary"/>
<meta name="twitter:title" content="An Introduction to JSON-LD"/>
<meta name="twitter:description" content="..."/>
</head>
From here various services know how to make reasonable previews of this page, for example, although some duplication is required. Facebook uses OpenGraph, while Twitter uses Twitter Cards. Sigh.
But JSON, by itself, doesn’t give us that. If you were an API client,
and you got the following blob of JSON (marked solely as
application/json
), would you know what it means?
{
"vq": 6253282,
"vq_fge": "6253282",
"anzr": "Gjvggre NCV",
"fperra_anzr": "GjvggreNCV",
"ybpngvba": "Fna Senapvfpb, PN",
"qrfpevcgvba": "Gur Erny Gjvggre NCV. Gjrrgf nobhg NCV punatrf, freivpr vffhrf naq bhe Qrirybcre Cyngsbez. Qba'g trg na nafjre? Vg'f ba zl jrofvgr.",
"hey": "uggcf:\/\/g.pb\/8VxPmPQe19",
"ragvgvrf": {
"hey": {
"heyf": [{
"hey": "uggcf:\/\/g.pb\/8VxPmPQe19",
"rkcnaqrq_hey": "uggcf:\/\/qrirybcre.gjvggre.pbz",
"qvfcynl_hey": "qrirybcre.gjvggre.pbz",
"vaqvprf": [
0,
23
]
}]
},
"qrfpevcgvba": {
"heyf": []
}
},
"cebgrpgrq": false,
"sbyybjref_pbhag": 6133636,
"sevraqf_pbhag": 12,
"yvfgrq_pbhag": 12936,
"perngrq_ng": "Jrq Znl 23 06:01:13 +0000 2007",
"snibhevgrf_pbhag": 31,
"irevsvrq": true,
"fgnghfrf_pbhag": 3656,
"cebsvyr_vzntr_hey": "uggcf:\/\/cof.gjvzt.pbz\/cebsvyr_vzntrf\/942858479592554497\/OonmYB9Y_abezny.wct",
"qrsnhyg_cebsvyr": false,
"qrsnhyg_cebsvyr_vzntr": false,
}
(This is a user object from the Twitter API, but with all the strings
ROT13‘ed). But this might as
well be what a programmatic client sees
when you hand it raw JSON. There’s structure here (scalars, arrays,
maps) but no meaning. What does the snibhevgrf_pbhag
field mean? In
fact, there are not even any hints that this came from the Twitter API!
Enter JSON-LD
JSON-LD is a
specification
being developed in the World Wide Web
Consortium (W3C) as a media type,
application/ld+json
. This means you can still parse it as a
plain-ol’ JSON document, but there are some additional conventions
that add meaning to the content. It turns out there are a bunch of
semantically-equivalent ways to annotate the relationships in
JSON-LD. For example, we can use IRIs in place of field names, as
a way of annotating a specific concept, like so:
{
"http://purl.org/dc/terms/identifier": [ 6253282, "6253282" ],
"http://purl.org/dc/terms/title": "Twitter API",
"http://xmlns.com/foaf/spec/#term_nick": "TwitterAPI",
"http://xmlns.com/foaf/spec/#term_based_near": "San Francisco, CA",
"http://schema.org/description": "The Real Twitter API. Tweets about API changes, service issues and our Developer Platform. Don't get an answer? It's on my website.",
"http://schema.org/url": "https:\/\/t.co\/8IkCzCDr19",
...
}
The interesting thing is that there exist a lot of standardized
ontologies for the Semantic Web that already have standard names for
certain concepts. For example, the notion of a “screen name” maps
pretty cleanly onto the FOAF term nick
. In fact, there is a whole
list of ontologies in the
recommended JSON-LD context.
But on the other hand, some
of the terms in the Twitter user object are specific to Twitter, like
default_profile_image
(whether the user still has the default
profile image or not). Now, it’d really be nice to be able to have a
clear identifier for that field and its semantics; I’d love to be able
to use an IRI like
https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/user-object#default_profile_image
for that, but (alas!) the field names on the Twitter documentation web
page don’t have id
attributes, so that anchor is not really a valid
target. (If someone at Twitter reads this, please mark these!).
But in any event, the problem with going back around and modifying the
underlying JSON in this way is that it would break all the existing
clients. So one option instead is to add a @context
field:
{
"@context" : {
"id" : "http://purl.org/dc/terms/identifier",
"id_str" : "http://purl.org/dc/terms/identifier",
"name" : "http://purl.org/dc/terms/title",
"screen_name" : "http://xmlns.com/foaf/spec/#term_nick",
"location" : "http://xmlns.com/foaf/spec/#term_based_near",
"description" : "http://schema.org/description",
"url" : {
"@id" : "http://schema.org/url",
"@type" : "@id"
},
},
"id": 6253282,
"id_str": "6253282",
"name": "Twitter API",
"screen_name": "TwitterAPI",
"location": "San Francisco, CA",
"description": "The Real Twitter API. Tweets about API changes, service issues and our Developer Platform. Don't get an answer? It's on my website.",
"url": "https:\/\/t.co\/8IkCzCDr19",
...
}
You can think of this as a “dictionary” explaining what each of the other JSON fields mean. But of course, we don’t really want to include that with each of our API responses. So another option is to locate the context itself at another URL and just refer to it there; for example, this might be perfectly reasonable:
{
"@context" : "https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/user-object.jsonld",
"id": 6253282,
"id_str": "6253282",
"name": "Twitter API",
"screen_name": "TwitterAPI",
"location": "San Francisco, CA",
"description": "The Real Twitter API. Tweets about API changes, service issues and our Developer Platform. Don't get an answer? It's on my website.",
"url": "https:\/\/t.co\/8IkCzCDr19",
...
}
assuming there was a JSON-LD context describing the user object at that location. Finally, if we can’t actually modify the response itself, we could potentially add an HTTP Link header like so:
Link: <https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/user-object.jsonld>;
rel="http://www.w3.org/ns/json-ld#context";
type="application/ld+json"
So this gives me a smooth upgrade path to start annotating the
responses with semantic information. Of course, if we were designing a
new API from scratch, instead of screen_name
would could use the
recommended JSON-LD context and use the field name foaf:nick
, since
JSON-LD supports compacted/shortened IRIs like this.
I expect this type of semantic annotation might be an important enabler for serendipitous reuse of APIs within an enterprise, and relying on existing public ontologies can cut down on the amount of documentation that might need to be written for an API.