Skip to main content

Data model

NGSI-LD is the ETSI standard API for context information, and it describes the world as a graph of entities. Each entity has an identifier, a type and attributes. An attribute either holds a value or points at another entity. This guide walks through the parts you write every day, with an example of each.

Entities​

An entity is one thing with an identity and a type, such as a vehicle, a building, a sensor or a parking lot. It is the unit you create, read, update and delete.

{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle"
}

The id is the entity's permanent name, and the address you use to read or change it. It must be an absolute URI. A URN under urn:ngsi-ld: is the usual choice, but any absolute URI works. Pick ids you can rebuild from what you already know, such as the plate number or the serial number of a device, so two systems that describe the same thing end up using the same id.

The type is required. It says what kind of thing the entity is, and it is a name that the @context expands, like an attribute name. An entity can have several types, written as an array such as ["Vehicle", "Asset"]. A query for any one of them finds it.

Everything else you know about the vehicle goes into attributes. An entity has no fixed shape. Two entities of one type can carry different attributes, and an attribute can be added or dropped later.

Two more members can sit beside id and type. A scope places the entity in a hierarchy that queries can filter on, see Scopes. @context gives the meaning of your names, see Names and the @context.

Attributes​

An attribute is a named piece of information about an entity, or about another attribute. Each one is a JSON object. Its type says what kind it is, and the kind decides which member holds the content.

NGSI-LD has eight kinds, two basic and six specialised. Property holds a value, and Relationship points at other entities. GeoProperty, LanguageProperty, JsonProperty, VocabProperty and ListProperty are Properties that hold a particular shape of value, and ListRelationship is the ordered form of a Relationship.

The kind matters because the broker treats each one differently. It indexes a GeoProperty for geoqueries, picks a language out of a LanguageProperty, leaves a JsonProperty's content alone, and expands a VocabProperty's term. The same coordinates in a plain Property can't be found by a geoquery, and the same text in a plain Property can't be picked by language.

Each kind below opens with a card that states what it holds, what to use it for, and how the broker treats it, followed by an example and the details. Here is one entity that uses every kind.

{
"id": "urn:ngsi-ld:Bus:42",
"type": "Bus",
"speed": {
"type": "Property",
"value": 52,
"unitCode": "KMH",
"observedAt": "2026-10-24T12:00:00Z"
},
"location": {
"type": "GeoProperty",
"value": {
"type": "Point",
"coordinates": [
2.35,
48.85
]
}
},
"operatedBy": {
"type": "Relationship",
"object": "urn:ngsi-ld:Operator:CityTransit"
},
"line": {
"type": "LanguageProperty",
"languageMap": {
"en": "Riverside Express",
"fr": "Express des Quais"
}
},
"category": {
"type": "VocabProperty",
"vocab": "publicTransport"
},
"diagnostics": {
"type": "JsonProperty",
"json": {
"firmware": "4.2.1",
"faults": [
{
"code": "E17",
"count": 2
}
]
}
},
"recentSpeeds": {
"type": "ListProperty",
"valueList": [
48,
50,
52
],
"unitCode": "KMH"
},
"route": {
"type": "ListRelationship",
"objectList": [
"urn:ngsi-ld:Stop:1",
"urn:ngsi-ld:Stop:2",
"urn:ngsi-ld:Stop:3"
]
}
}

An attribute written without a type is read from the member it carries. A value makes it a Property, or a GeoProperty when the value is a GeoJSON geometry. A languageMap, vocab, json, valueList, object or objectList makes it the kind that owns that member. An attribute can carry only one of these members.

Property​

A Property attaches a value to the entity, in value. It is the kind to reach for first, and the right one for most of what a sensor or an application reports.

"speed": {
"type": "Property",
"value": 52,
"unitCode": "KMH",
"observedAt": "2026-10-24T12:00:00Z"
}

What you choose for the value decides what you can do with it later. A number or a string can be compared, ordered and matched: q=speed>50. An array holds several values at once, such as ["red", "green"], and a query asks whether it holds an operand: q=tags=="red". An object holds a structured value, such as an address, and a query addresses one member of it: q=address[city]=="Berlin". Member names are terms of your @context, like attribute names.

Beside value, a Property can say more about it. unitCode gives the unit, observedAt says when it was measured, and valueType declares the datatype of a value that JSON can't tell, such as a DateTime. A declared datatype has to fit the value. See Attributes on attributes.

A Property records a fact about the entity, not a link to another one. If the value is the id of another entity, you probably want a Relationship.

GeoProperty​

A GeoProperty holds a GeoJSON geometry in value, a Point, LineString, Polygon, or one of the multi-part types such as MultiPolygon.

"location": {
"type": "GeoProperty",
"value": {
"type": "Point",
"coordinates": [
2.35,
48.85
]
}
}

Use it for anything that has a position or a footprint, such as where a vehicle is, the outline of a parking lot, the route of a delivery, or the area a sensor covers. The specification reserves three names for the common cases. location is the entity's position, observationSpace is the area a sensor observes, and operationSpace is the area in which a device or an actor works. Any other name works the same way.

Every GeoProperty can be the target of a geoquery, which finds entities near a point, inside an area, or overlapping a shape. See Geoqueries. A Property that happens to hold GeoJSON can't, since the broker does not index it.

A position can carry an altitude as a third number. A geometry has altitude throughout or nowhere, never a mix, and geoqueries look only at the footprint on the ground. GeometryCollection isn't supported. Geometry and geoqueries has the rules for what the broker accepts.

Relationship​

A Relationship points at another resource, usually another entity. Its object is the URI at the other end, or an array of URIs.

"operatedBy": {
"type": "Relationship",
"object": "urn:ngsi-ld:Operator:CityTransit"
}

A Property records a fact about the entity. A Relationship records an edge from this entity to another one, so the data forms a graph instead of a pile of separate records. Use one wherever the value would be another entity's id, for who owns a car, which building a room is in, or which device produced a reading.

The target is a name, not a pointer the broker owns. It can be an entity this broker holds, an entity that another broker holds and the broker reaches through a context source, or an external resource that has a URI but isn't an entity, such as a page on DBpedia. Only an entity can be followed with join. The broker never decodes or rewrites a target.

A Relationship can have several targets. Write object as an array when the targets share one set of sub-attributes, or give each target its own datasetId when each needs its own. The targets of one Relationship have no order. For an order, use a ListRelationship.

Queries can follow the edge. With join set, q=operatedBy{city}=="Paris" selects buses whose operator is in Paris, and a read can return the linked entities inside the answer. Linked entities explains how.

LanguageProperty​

A LanguageProperty holds the same text in several languages, in a languageMap keyed by language tag.

"name": {
"type": "LanguageProperty",
"languageMap": {
"en": "Town Hall",
"fr": "Hôtel de ville"
}
}

Without it you would invent an attribute per language, such as name_en and name_fr, and every client would have to know the convention.

Keys are BCP 47 language tags, so en, fr-CA and de-CH-1901 are all legal, and en_US is not. An entry keyed @none marks the text that belongs to no language, and is the fallback of last resort on a read. A language can hold several strings, written as an array, as in "en": ["Town Hall", "City Hall"].

A read returns the map as stored. A read with lang=fr returns the French entry as a plain Property instead, so a client doesn't have to carry every translation and pick one. Multilingual queries covers lang, preferences and fallbacks.

VocabProperty​

A VocabProperty holds a term from a vocabulary, in vocab. The term is expanded against the @context like a name, so it stands for the same IRI everywhere.

"category": {
"type": "VocabProperty",
"vocab": "publicTransport"
}

A Property holding "publicTransport" holds the characters. A VocabProperty holds the concept, so two systems with different short names for it, or with different languages, still agree that it is the same thing. The mapping is in the @context. With this one, publicTransport stands for the IRI on the right.

"@context": [
{
"publicTransport": "https://example.org/vocab/PublicTransport"
},
"https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.9.jsonld"
]

vocab can hold one term or an array of them, for an entity that belongs to several categories. A query compares the full IRI, as in q=category=="https://example.org/vocab/PublicTransport". To write the short term instead, quote it and name the attribute in expandValues, as in q=category=="publicTransport"&expandValues=category. See The q language. A reader whose @context gives the IRI another term sees that term.

JsonProperty​

A JsonProperty holds JSON that the broker keeps as it is, in json. Nothing inside it is expanded against the @context, so its keys don't have to be URIs or terms, and a key named id or value is only a key.

"diagnostics": {
"type": "JsonProperty",
"json": {
"firmware": "4.2.1",
"faults": [
{
"code": "E17",
"count": 2
}
]
}
}

Use it for payloads you want to give back unchanged and whose inner names you don't want to turn into terms, such as a GeoJSON feature you don't want indexed. Use a plain Property instead when you want the members of the object to be real attributes of your model.

The value must be an object, or an array of objects. A string, a number or null is refused. A query reads a member by its key as written, and a dotted path goes deeper, as in q=diagnostics[firmware]=="4.2.1"&jsonKeys=diagnostics.

ListProperty​

A ListProperty holds an ordered list of values, in valueList. The order you write is the order you get back, and it is part of the meaning.

"recentSpeeds": {
"type": "ListProperty",
"valueList": [
48,
50,
52
],
"unitCode": "KMH"
}

Each item is a value like a Property's, and numbers, strings, booleans and objects all work in one list. A Property that holds an array suits a group of values you only ask about, such as tags. Use a ListProperty when the sequence itself is the data.

ListRelationship​

A ListRelationship holds an ordered list of entity URIs, in objectList.

"route": {
"type": "ListRelationship",
"objectList": [
"urn:ngsi-ld:Stop:1",
"urn:ngsi-ld:Stop:2",
"urn:ngsi-ld:Stop:3"
]
}

A Relationship with several targets gives you no order, and a list of ids in a ListProperty gives you ids that nothing can follow. A ListRelationship gives you both the order and the edges. Every item is an absolute URI. With join set, q=route{name}=="Central" selects the buses that pass a stop named Central.

Attributes on attributes​

Any attribute can carry attributes that describe it. A few names have a fixed meaning.

  • observedAt is when the value was observed, as a UTC date-time.
  • unitCode is the unit of the value, as a UN/CEFACT common code, such as KMH for kilometres per hour.
  • valueType is the datatype of the value, for a value JSON can't tell by itself.
  • datasetId says which instance of the attribute this is, when the entity has several. See below.
"speed": {
"type": "Property",
"value": 52,
"unitCode": "KMH",
"observedAt": "2026-10-24T12:00:00Z",
"reliability": {
"type": "Property",
"value": 0.95
}
}

unitCode fits the kinds that hold a measurable value, namely Property, GeoProperty and ListProperty. A Relationship, ListRelationship, LanguageProperty, VocabProperty or JsonProperty has nothing to measure, and a unitCode on one is refused.

reliability is an ordinary Property nested inside another. Nesting can go on, up to request_limits.max_attribute_nesting levels, 32 by default.

The broker keeps createdAt and modifiedAt on entities and attributes itself. A value you write for them is replaced. You can ask for them back with options=sysAttrs. Values covers how numbers, date-times and units are stored and served.

Several instances of one attribute​

An entity can hold more than one instance of the same attribute at once, such as a temperature from two sensors. Each instance has its own datasetId, which is a URI.

"temperature": [
{
"type": "Property",
"value": 21.2,
"datasetId": "urn:ngsi-ld:Dataset:sensor-a"
},
{
"type": "Property",
"value": 21.6,
"datasetId": "urn:ngsi-ld:Dataset:sensor-b"
}
]

At most one instance can leave datasetId out, and that one is the default instance. Writes, reads and history can each target one instance by its datasetId.

Names and the @context​

Inside the broker, every name is a full IRI. The @context maps your short names, such as speed or Vehicle, to IRIs when you write, and the broker maps them back to short names when it answers. JSON-LD explains how that works, and how to send and store a @context.

Representations​

You choose the shape of an answer per request. The stored entity is the same either way.

  • Normalized, the default. Every attribute is an object with its type and value, as in the examples above.
  • options=concise drops what can be inferred. A Property that has only a value becomes the value. One with sub-attributes keeps its object without type. Nothing is lost.
  • options=keyValues, also called simplified. Every attribute becomes its value alone, and a Relationship its target. Sub-attributes are dropped, and you can no longer tell a Property from a Relationship.

What keyValues leaves of each kind is as follows.

  • Property and GeoProperty become the value or the geometry.
  • Relationship becomes the target URI, or an array of them.
  • ListProperty and ListRelationship become the array.
  • LanguageProperty keeps {"languageMap": {...}}, or the plain string when you ask for a lang.
  • VocabProperty keeps {"vocab": "publicTransport"}.
  • JsonProperty keeps {"json": {...}}.

The last three keep their wrapper because a bare value couldn't tell them from a Property holding the same data. Here is the bus from Attributes as keyValues.

{
"id": "urn:ngsi-ld:Bus:42",
"type": "Bus",
"speed": 52,
"location": {
"type": "Point",
"coordinates": [
2.35,
48.85
]
},
"operatedBy": "urn:ngsi-ld:Operator:CityTransit",
"line": {
"languageMap": {
"en": "Riverside Express",
"fr": "Express des Quais"
}
},
"category": {
"vocab": "publicTransport"
},
"diagnostics": {
"json": {
"firmware": "4.2.1",
"faults": [
{
"code": "E17",
"count": 2
}
]
}
},
"recentSpeeds": [
48,
50,
52
],
"route": [
"urn:ngsi-ld:Stop:1",
"urn:ngsi-ld:Stop:2",
"urn:ngsi-ld:Stop:3"
]
}

Use keyValues for a client that only displays data. Use normalized when you need the sub-attributes, or when you will write the entity back.

The Temporal API has a fourth shape, where each attribute carries its history. The temporal guide covers it.