JSON-LD @context
Short names mean nothing on their own. A @context is what turns Building into an IRI, so the @context a request is read with decides what the written data means. Andromeda therefore shows its choice. Every response names the @context it applied, the ones you sent in your order, with the v1.9 core @context at the end.
This page covers how that value is built for responses and notifications, and how Andromeda fetches, caches and hosts @context documents.
The @context of a response
Say you send a request under your own context and ask for application/ld+json.
GET /ngsi-ld/v1/entities/urn:ngsi-ld:Building:1
Accept: application/ld+json
Link: <https://example.org/building.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
HTTP/1.1 200 OK
Content-Type: application/ld+json
{
"@context": [
"https://example.org/building.jsonld",
"https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.9.jsonld"
],
"id": "urn:ngsi-ld:Building:1",
"type": "Building"
}
The same rule applies to every application/ld+json response and every application/geo+json envelope. An application/json response has no @context in its body and names one in a Link header instead, which its own section covers.
The value is built in five steps.
- Your contexts stay in the order you named them, repeats included. JSON-LD reads an array in order, so
[A, B, A]leaves A's definitions in force, where[A, B]leaves B's. - Any core
@contextyou named is taken out, whatever release it is and wherever it stands, even inside an inline context that imports it. - The v1.9 core
@contextgoes on the end. There is one exception. If you named no core@contextyourself and your last context already ends with v1.9, directly or through what it references, nothing is added. - If nothing is left, the
@contextis the v1.9 core@contextalone. - One context is written as a string, several as an array.
The core context is last for a reason. JSON-LD lets a later context in an array override an earlier one, so whichever context is written last wins. The specification pins the core @context to that position so that upstream processors "can properly expand as NGSI-LD" (clause 4.4).
Your context imports the core
Your own context may import the core @context and then define terms of its own. Even then the core @context is added after it, and the placement matters. JSON-LD protects core terms from redefinition, but it doesn't protect @vocab, so a processor reading only your context could expand things differently from Andromeda, which always applies the core @context last.
Your context ends with an older release
A context that ends with an older core release, such as v1.8, is still followed by v1.9. A strict JSON-LD processor downstream may then reject the result, because v1.9 redefines terms that v1.8 protects (attributeCount, attributeDetails, entityMap). The fix is on your side. Bring the context up to v1.9.
application/json responses
An application/json response names its @context in exactly one Link header. What it points at depends on the @context built above.
If that is a single URL, the link names it. That happens when you named no context, only a core release, or one context that already ends with the v1.9 core @context.
Otherwise, Andromeda stores a wrapper document, {"@context": <the value>}, and links to that. Several contexts, an inline context, and a single context that doesn't end with the core @context all land here. The wrapper exists so that a JSON-LD processor following the link still reads the core @context last, exactly as it would have from a body.
HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://broker.example.org/ngsi-ld/v1/jsonldContexts/urn:ngsi-ld:JsonLdContext:...>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
Wrappers are stored as ImplicitlyCreated entries, and equal wrappers share one entry, so a request repeated a thousand times stores one document. If a wrapper can't be stored, the response fails with 503 or 500. You never get a 200 that is missing its Link.
Only a response that has a body and puts its context in a Link gets a wrapper. That means application/json, and application/geo+json with Prefer: body=json. A response without a body, a 207 and an error name no context at all.
Notifications
A notification uses the subscription's jsonldContext, placed by the same rules as a response.
- An
application/ld+jsonnotification carries it on the notification and on every entity indata. - An
application/geo+jsonnotification carries it on its FeatureCollection only, never on the notification itself or on a single Feature. WithPrefer: body=jsoninreceiverInfo, the FeatureCollection doesn't carry it either, and theLinkheader names it instead. - An
application/jsonnotification names it in itsLinkheader.
If the jsonldContext is itself a core @context, of any release, it is used on its own, exactly as written.
A subscription created without jsonldContext takes the @context of the request that created it. That is the URL itself when it was one URL, and a hosted wrapper otherwise. Context source registration subscriptions work the same way.
If the jsonldContext can't be loaded when a notification is due, Andromeda doesn't guess. The delivery fails and is retried like any other failed delivery.
Stored @context documents and their URLs
Andromeda hosts @context documents under /ngsi-ld/v1/jsonldContexts/<identifier>. Every URL it hands out for one, whether for a stored entry, a wrapper Link or a subscription's jsonldContext, is absolute, in the form <apiRoot>/ngsi-ld/v1/jsonldContexts/<identifier>.
The apiRoot is server.public_url if you set it, path prefix included. Otherwise it is the scheme and host the request came in on, or those a proxy the broker believes forwarded (http.forwarded.trust). Behind a proxy, set one of the two, or clients may get URLs they can't reach. TLS has the details.
Stored @contexts belong to a tenant, even though the URL doesn't name one. A tenant's entry is served only to requests carrying that tenant's NGSILD-Tenant header, like any other resource of the tenant.
When a request refers to one of Andromeda's own stored @contexts, the broker reads the tenant's entry straight from storage, whatever host the URL names, without an HTTP request.
A URL shaped like one of its own that the tenant doesn't hold is another matter. It may belong to another broker, or to another tenant of this one, so Andromeda resolves it like any other URL, even when it names the broker itself. That fetch carries no tenant and goes through whatever authentication and proxy sit in front of the URL, so it succeeds only where any client could fetch the URL. The download is held in memory for the max-age the response states, and it is never recorded as a Cached entry.
The core @context is built in. Every reference to it names its ETSI URL, and it isn't a stored entry. No listing returns it, /jsonldContexts/ with any of its URLs answers 404 ResourceNotFound, and no delete or reload touches it. Andromeda resolves it from the embedded copy without any network, so an airgapped broker behaves the same.
The W3C Data Integrity context, https://w3id.org/security/data-integrity/v2, is built in too, and for the same reason. The core @context names it inside ngsildproof, which makes it part of the core @context in practice. It isn't a stored entry either. No listing returns it, /jsonldContexts/ with its URL answers 404, and resolving it never leaves a Cached entry behind.
Deleting an ImplicitlyCreated entry breaks the Link URL of every subscription that shares it. The next request or subscription with the same content stores it again under the same identifier, and Identifiers explains why the identifier comes out the same.
Fetching remote @context documents
When a request names a @context that isn't one of Andromeda's, the broker fetches it. The document can arrive as application/json, application/ld+json, any +json type, text/plain, or with no Content-Type at all. text/plain is on the list because data model contexts are often served that way. Whatever the media type, the body must be UTF-8 JSON with a top-level @context.
If Andromeda reached the document through a redirect or a JSON-LD alternate link, relative references inside it resolve against the URL it was finally read from.
Fetched documents are cached. The response's Cache-Control (max-age, s-maxage) and Expires decide how long, and a response that sets neither is kept for jsonld.cache.default_lifetime.
With jsonld.fetch.mode set to "disabled", nothing remote is fetched at all. Andromeda works from the core @context, the W3C Data Integrity context, the documents it stores and whatever is already in its cache.
Core @context releases
Andromeda carries the core @context releases v1.0 and v1.3 to v1.9, each one byte for byte as ETSI publishes it. ETSI publishes v1.0 under the URL without a version number, https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context.jsonld.
A request may name any of these releases, in its Link header or in its body. Andromeda takes the release out and processes the whole request as v1.9. Every response body is compacted with v1.9, so responses always name the v1.9 URL below and never the release you sent.
https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.9.jsonld
The same happens inside a @context document of your own. If it imports one of these releases, the import is dropped and the document's terms are read under v1.9 as well. A term that only an older release defines then falls through to the v1.9 default @vocab. The v1.0 term name, for example, expands to https://uri.etsi.org/ngsi-ld/default-context/name.
Proofs
An ngsildproof Property holds a W3C Data Integrity proof as its value. Andromeda reads the members inside it under the W3C Data Integrity context, so proof, DataIntegrityProof, created, cryptosuite, proofPurpose, proofValue, verificationMethod and the other terms that context defines expand to their W3C IRIs. On the way out they compact back to the terms you wrote. A retrieval returns the proof as you sent it, in every representation and through the Temporal API as well.
None of this needs a network. The embedded copy is byte for byte the document the W3C publishes and matches the SHA2-256 digest the W3C lists for it, so Andromeda never fetches it, whatever jsonld.fetch.mode is set to. If your own @context names the W3C URL directly, that reference resolves to the embedded copy too.
Andromeda doesn't verify signatures and doesn't create them. It stores the proof and hands it back unchanged.
No other W3C or third-party context is built in. The Multikey and JWK contexts, for instance, are fetched or preloaded like any other @context.
What gets refused
| Response | When | What to do |
|---|---|---|
504 LdContextNotAvailable | A remote @context can't be fetched, is larger than jsonld.fetch.max_size, or has a URL, redirect or alternate that resolves to an address jsonld.fetch.private_addresses, jsonld.fetch.allowed_addresses and jsonld.fetch.denied_addresses refuse | Check that the URL is reachable from the broker and the document isn't too big |
400 BadRequestData | References loop, or go more than eight documents deep | Untangle the chain. Andromeda stops before fetching the next document |
The rules your own @context has to follow are under JSON-LD @context in Strictness.
Configuration
| Key | Default | What it does |
|---|---|---|
jsonld.fetch.max_size | 2 MiB | The largest @context document Andromeda fetches |
jsonld.cache.default_lifetime | "1h" | How long a fetched document is kept when its response sets no Cache-Control or Expires |
jsonld.fetch.mode | "enabled" | "disabled" stops all remote fetches, as described under Fetching remote @context documents. The core @context and the W3C Data Integrity context stay available |
server.public_url | no default | The apiRoot of every @context URL Andromeda hands out. Set it behind a proxy |
http.forwarded.trust and http.forwarded.proxies | "none" and empty | Whose forwarded fields decide the scheme and host of those URLs when server.public_url is not set |
Where Andromeda interprets the specification
The specification has an application/ld+json entity, registration, subscription or notification carry "a user @context where present, and the core @context". It treats the core @context as always last, and names the v1.9 URL as the core @context.
The single Link header of an application/json response follows the HTTP binding and JSON-LD 1.1. The wrapper document follows the examples, and wrappers are ImplicitlyCreated entries. The binding also says which responses name no context.
Notifications take their context from the subscription, and context source registration subscriptions from their own clause. The specification also fixes where a GeoJSON notification carries it.
The core @context is read as not stored. The specification lists "@contexts currently stored", which excludes the embedded copy. It answers the core URLs ResourceNotFound, while keeping the core @context the one every answer names.
The v1.9 core @context names the W3C Data Integrity context in the scoped context of ngsildproof. W3C Data Integrity publishes a digest for that document and lets a processor treat its URL as already resolved when the copy matches, which is why Andromeda never fetches it. Andromeda reads the embedded copy as part of the core @context, which the specification calls protected, so the scoped contexts it declares are accepted where a user @context declaring them would be refused.
The specification sets no media type for a remote @context. Alternate links come from JSON-LD 1.1 and cache lifetimes from the HTTP binding. Hosting is the JSON-LD @context API, with deletion defined separately.