Skip to main content

JSON-LD

Every NGSI-LD request and answer is JSON-LD, ordinary JSON with one extra member, @context, that says what the names in it mean. You can write NGSI-LD for a while without thinking about it, because the broker fills in sensible defaults. Sooner or later, though, a query matches nothing or an answer comes back full of long URLs, and the @context is nearly always why.

This guide explains what JSON-LD is, how it turns short names into meanings, and why NGSI-LD uses it. It then shows how to send a @context, how to store one on the broker, and what to expect when you read data back.

Names need meanings​

Say two systems report on the same car. One writes as follows.

{
"brandName": "Mercedes"
}

The other, built by a French team, writes as follows.

{
"marque": "Mercedes"
}

A person sees that both mean the same thing. A program doesn't. And the opposite problem is just as real. Two systems can both write temperature, one meaning the air in a room and the other the body of a patient. Plain JSON has no way to say what a key means, so every integration ends up with a hand-written mapping between the names one system uses and the names another expects.

That is manageable inside one application. It falls apart when data from many cities, vendors and devices meets in one place, which is exactly what a context broker is for.

IRIs as names​

The fix that the web settled on is to name things with IRIs, the general form of a URL. Instead of brandName, the name is https://example.org/vehicle/brandName. An IRI is globally unique, because whoever owns the domain decides what is published under it, so two vocabularies can't collide by accident. It can also lead somewhere, since a well-run vocabulary serves a definition at that address.

Nobody wants to write full IRIs as JSON keys, though, and nobody wants to read them. JSON-LD keeps the short names you are used to and moves the IRIs into a separate dictionary, the @context.

The @context​

A @context maps short names, which JSON-LD calls terms, to IRIs.

{
"@context": {
"brandName": "https://example.org/vehicle/brandName"
},
"brandName": "Mercedes"
}

A JSON-LD processor reads that document by expansion. It replaces every term with the IRI the @context gives it and drops the @context, since its job is done. The result is what the document actually says.

[
{
"https://example.org/vehicle/brandName": [
{
"@value": "Mercedes"
}
]
}
]

Now take the French document with a @context of its own:

{
"@context": {
"marque": "https://example.org/vehicle/brandName"
},
"marque": "Mercedes"
}

It expands to exactly the same thing. The two systems use different words, but they map them to the same IRI, so they say the same thing, and a program can tell.

Compaction is the way back. It takes expanded data and a @context, and writes each IRI as the term that @context gives it. Compact the expanded document above with the first @context and you get brandName; compact it with the second and you get marque. So the writer and the reader don't need to share short names, only IRIs. Each side picks its own @context.

A few more things are worth knowing before NGSI-LD enters the picture.

  • A @context is usually not written inline but published as a document, referenced by its URL, as in "@context": "https://example.org/vehicle.jsonld". Vocabularies such as the Smart Data Models publish theirs this way.
  • A @context can be an array of URLs and inline objects. They apply in order, and when two of them define the same term, the later one wins.
  • @id and @type are JSON-LD keywords, not terms. They mean "the identifier of this thing" and "what kind of thing it is" in every document.
  • A term that no @context defines can still be expanded if a @context sets a default namespace, called @vocab. Without one, the processor drops the member.

Why NGSI-LD is built on JSON-LD​

NGSI-LD is meant for data that many parties produce and many others read, and JSON-LD is what lets that work.

  • Data from different producers fits together. Two entities that use different short names for the same IRI are queried, filtered and subscribed to as one.
  • You query by meaning, not by spelling. A query names terms, the broker expands them under your @context, and it matches the IRIs, whatever words the writers used.
  • It is standard linked data. NGSI-LD entities convert to RDF and can point at published vocabularies, so they plug into tools far beyond NGSI-LD. That is the "LD" in the name.

NGSI-LD's own words, such as id, type, Property, Relationship, value, object and observedAt, are terms too. They are defined by the core @context, which ETSI publishes at the following address.

https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.9.jsonld

The core @context maps id to @id and type to @type, value to https://uri.etsi.org/ngsi-ld/hasValue, and so on. It differs from any @context you write.

  • It always applies. You never have to send it, and if you do, the broker treats it as if it came last.
  • It is protected. Your @context can't redefine type, value or any other core term.
  • It sets @vocab to https://uri.etsi.org/ngsi-ld/default-context/, so a name your @context doesn't define still means something, and brandName with no definition becomes https://uri.etsi.org/ngsi-ld/default-context/brandName.

Your own @context sits in front of it and defines the rest, one term for each entity type and one for each attribute name you use.

What the broker does with your names​

The broker keeps no short names. Every write is expanded with the @context of the request, and what is stored is the expanded data, where the entity's type, its attribute names and the NGSI-LD terms are all IRIs.

Every answer is compacted again, with the @context of the request that reads it. That is the reader's @context, not the writer's. The broker never remembers which @context an entity was written with.

This catches most people out at least once.

  • A reader who sends the writer's @context sees the writer's names.
  • A reader who sends a different @context that maps the same IRIs sees their own names.
  • A reader who sends no @context sees full IRIs for every name that only the writer's @context defined.
  • A query is expanded too. type=Vehicle with no @context asks for https://uri.etsi.org/ngsi-ld/default-context/Vehicle, which matches nothing that was written as https://example.org/vehicle/Vehicle.

So decide on one @context and send it on every request, writes, reads, queries and subscriptions alike.

The examples below create Vehicle A4567, the car from the specification's own examples, under this @context.

{
"Vehicle": "https://example.org/vehicle/Vehicle",
"brandName": "https://example.org/vehicle/brandName"
}

Sending a @context in the body​

The first way to send a @context is the JSON-LD way, putting it in the body as an @context member and saying so with Content-Type: application/ld+json.

POST /ngsi-ld/v1/entities
Content-Type: application/ld+json

{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"brandName": {
"type": "Property",
"value": "Mercedes"
},
"@context": {
"Vehicle": "https://example.org/vehicle/Vehicle",
"brandName": "https://example.org/vehicle/brandName"
}
}
HTTP/1.1 201 Created
Location: /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567

The body form takes any @context, whether a URL, an inline object as here, or an array of both. Inline is handy for trying things out, but the specification recommends against it for real use. A broker caches and shares a @context by its URL, and an inline one has none, so every client has to carry its own copy in every request. Publish your @context at a URL instead, on your own server or on the broker itself.

Storing a @context on the broker​

The broker can host your @context for you, so you don't need a web server just to publish one. Send the document, an object with a top-level @context, to /ngsi-ld/v1/jsonldContexts.

POST /ngsi-ld/v1/jsonldContexts
Content-Type: application/json

{
"@context": {
"Vehicle": "https://example.org/vehicle/Vehicle",
"brandName": "https://example.org/vehicle/brandName"
}
}
HTTP/1.1 201 Created
Location: /ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f

The broker gives the document an identifier of its own, and the Location is where it now serves it. Yours will have a different identifier. Prefix the Location with the broker's address and you have the URL to use from now on.

http://localhost:1056/ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f

Any member beside @context in the body you send is discarded, as JSON-LD requires. A body without a top-level @context is refused with 400 BadRequestData, and nothing is stored.

A GET on that URL answers the document as you stored it. To see everything you have stored, list the Hosted entries.

GET /ngsi-ld/v1/jsonldContexts?kind=Hosted
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json

[
"http://localhost:1056/ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f"
]

Hosted is one of three kinds. The broker also keeps Cached copies of the remote @contexts it has downloaded, and ImplicitlyCreated documents it made itself, for instance to give an array of contexts a single URL. Leave out kind to list them all, and add details=true to get each entry's identifier, kind, creation time and usage counts instead of the bare URL.

Stored @contexts belong to a tenant, although the URL doesn't name one. A request for another tenant doesn't find it, so send the same NGSILD-Tenant header you stored it with.

When you no longer need one, delete it by its identifier.

DELETE /ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f
HTTP/1.1 204 No Content

Deleting the document changes nothing that was written with it. Entities hold IRIs, not the @context that produced them.

With the @context at a URL, you can keep it out of the body altogether. Send the body as plain application/json and name the @context in a Link header.

Link: <http://localhost:1056/ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"

The URL goes between the angle brackets. The rel value is fixed. It is what marks this link as a JSON-LD @context rather than any other kind of link. Here is the same vehicle created that way.

POST /ngsi-ld/v1/entities
Content-Type: application/json
Link: <http://localhost:1056/ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"

{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"brandName": {
"type": "Property",
"value": "Mercedes"
}
}

The Rust client derived with with_context sends the Link on every request it makes, and the Python examples below pass LINK the same way.

The Link header is also the only way to give a @context to a request that has no body, which is every GET and DELETE, including every query. So even if you write with @context in the body, your reads use the header.

Which form a request uses is decided by its method and Content-Type, and the two are never mixed.

RequestWhere the @context goes
GET, DELETELink header
POST, PATCH, PUT with application/jsonLink header, and the body must not hold @context
POST, PATCH, PUT with application/ld+json@context member of the body, and no Link header

A request with neither gets the core @context alone, so every name it uses falls into the default namespace. A Link header names one URL, which is why an inline @context or an array has to travel in an application/ld+json body, or be stored first so that it has a URL of its own.

Reading data back​

Read the vehicle with the stored @context and ask for application/ld+json, and the answer carries its @context in the body.

GET /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567
Accept: application/ld+json
Link: <http://localhost:1056/ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
HTTP/1.1 200 OK
Content-Type: application/ld+json

{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"brandName": {
"type": "Property",
"value": "Mercedes"
},
"@context": [
"http://localhost:1056/ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:2fc7f63e-9213-4821-8720-65b9a404df6f",
"https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.9.jsonld"
]
}

The answer names your @context first and the core @context last, so the document stands on its own, and any JSON-LD processor that reads it expands it to the same IRIs the broker stored. With Accept: application/json the body is the same without the @context member, and the @context is named in a Link header of the answer instead.

Now read the same vehicle without a Link header. The core @context alone can't compact the vehicle's own IRIs, so they come back in full.

GET /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json

{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "https://example.org/vehicle/Vehicle",
"https://example.org/vehicle/brandName": {
"type": "Property",
"value": "Mercedes"
}
}

Nothing is wrong with the entity. It is the same data, written without the short names that only your @context knows. NGSI-LD's own terms, id, type and value, still read short, because the core @context always applies.

The same thing happens the other way round. A vehicle written with no @context at all is stored under the default namespace, as https://uri.etsi.org/ngsi-ld/default-context/brandName. Read without a @context, it looks fine, because the default namespace compacts back to brandName. Read with the @context above, which gives brandName a different IRI, it comes back as ngsi-ld:default-context/brandName, and its type as ngsi-ld:default-context/Vehicle. If you see names like these, someone wrote without the @context you read with.

Queries follow the same rule, because type, attrs, q and the other parameters that name terms are expanded with the request's @context. GET /ngsi-ld/v1/entities?type=Vehicle without the Link header answers [], because it asks for the default namespace's Vehicle. Subscriptions do too, and each one keeps a jsonldContext, the @context its notifications are compacted with. Subscriptions shows how to set it.

Writing a good @context​

  • Use IRIs you control, or terms from a published vocabulary such as the Smart Data Models. An IRI under example.org is fine for trying things out and nothing else.
  • Define every entity type and every attribute name you use. A name you leave out falls into the default namespace, where anyone else's undefined brandName means the same as yours.
  • Never change what a published @context maps a term to. Data already written keeps the old IRI, so the same short name would then mean two things. Publish a new version under a new URL instead.
  • Don't try to redefine core terms. The core @context is protected, so type, value and the rest keep their NGSI-LD meaning whatever your @context says.
  • Share one @context across every client that reads and writes the same data, and send it on every request.

What gets refused​

  • 400 BadRequestData for an application/json body that holds @context.
  • 400 BadRequestData for an application/ld+json body without @context, or one sent with a Link header naming a @context.
  • 400 BadRequestData for more than one @context in Link headers.
  • 400 BadRequestData for a @context stored without a top-level @context member.
  • 504 LdContextNotAvailable for a @context URL the broker can't fetch.

JSON-LD @context in Strictness lists every rule a @context has to follow.

Where next​

  • Data model

    The entities, attributes and representations that the names in this guide belong to.

  • JSON-LD @context

    How the broker builds the @context of each answer and notification, and how it fetches, caches and hosts @context documents.

  • JSON-LD configuration

    Limits and switches for fetching remote @contexts.

  • jsonld

    The JSON-LD processor behind these steps, and how the broker keeps the cost of a @context down.