Skip to main content

Attributes and null values

Attributes​

An attribute instance's type is one of the eight attribute types, matched case-sensitively, so property is refused. An instance can't declare two types. The rule holds at every depth, because a sub-attribute is an attribute. It also holds for any object written where an attribute stands, since an object with a type there is read as a normalized attribute. That is why {"type": "Feature"}, a type that isn't a string, and a GeoJSON geometry type without its coordinates are all refused. A type inside a Property's value is data, and it isn't read.

An attribute written without type carries the value member of one attribute type at most, out of object, languageMap, vocab, valueList, objectList and json. So {"object": …, "languageMap": …} is refused. An attribute written with value instead is a Property, or a GeoProperty when the value is a geometry.

An array written where an attribute stands is either the instances of a multi-attribute, where every element is an attribute object, or a single Property value, where no element is. An array that mixes the two, such as [{"value": 25, "datasetId": "urn:ngsi-ld:Dataset:a"}, 30], is refused.

An instance can't carry a member its attribute type doesn't define. That rules out value on a Relationship, object on a Property, two value members on one instance, valueType on a Relationship or ListRelationship, and objectType on anything but a Relationship.

An instance does carry the value member its type requires. That is value for a Property or GeoProperty, object for a Relationship, and the matching one of languageMap, vocab, json, valueList or objectList for the other five types.

vocab holds name references and json holds a JSON literal, and neither can be empty. valueList and objectList each hold exactly one list.

A JsonProperty's json is a JSON object or an array of JSON objects, and the array may be empty. A scalar, null, or an array holding anything but objects is refused, on attributes and sub-attributes alike.

An attribute holds at most one default instance, the one without a datasetId, and no two of its instances share a datasetId. Sub-attributes follow the same rule.

A partial attribute update can't change an instance's attribute type.

Members that only a response carries can't be written. These are the Temporal API plurals inside an instance (values, objects and so on), the previous… member of another attribute type, the inline linked entity members entity and entityList, and an @id on an attribute instance.

The data-integrity seals entityIdSealed and entityTypeSealed appear only inside ngsildproof.

The JSON-LD document a write expands to is a single entity node, so @graph, @included, @reverse and @index are refused.

Keys inside a value​

A Property value can be any JSON value, and the keys of an object inside it don't have to follow the name grammar. {"opening hours": "9-5"}, a relative {"id": "abc"} and {"type": "Postal Address"} are stored and read back as written, and so is a geometry's foreign member with a space in its name.

Two kinds of key are refused, because Andromeda has to return the value as written and JSON-LD expansion can't keep either of them.

  • A key that looks like a JSON-LD keyword without being one, meaning @ followed by letters only, such as @foo. Expansion drops it. This applies at every level of a payload.
  • An empty key, or an empty type, below the top level. Expansion turns it into the @vocab IRI.

The keys of a languageMap are language tags, and Language tags has their rules. A JsonProperty's json is carried verbatim, so its keys are exempt.

JSON null​

A JSON null is refused wherever a request body holds one, at any depth, and the error names the member. That goes for entities, attribute fragments, batch items, temporal evolutions, subscriptions, registrations, Snapshots, EntityMaps and Query bodies, optional members included. "datasetId": null doesn't mean the default instance, and "location": null doesn't delete anything. To delete something, send the NGSI-LD Null described below.

Three places hold JSON as data and keep a null inside it.

  • A JSON literal, meaning a @json value, the json of a JsonProperty below its top level, or a Context Source Property below its top level.
  • A hosted @context document.
  • A generated member Andromeda doesn't read, such as the status of a subscription you read and send back. Every other member of a subscription body is either read or refused.

No data type takes null for any of its members. Deleting is the NGSI-LD Null's job.

The NUL character​

A request with U+0000 anywhere in its text is refused with BadRequestData, on every endpoint. JSON allows the character (as \u0000), a query string carries it as %00, and the specification has no rule against it. Andromeda's store can't hold it, though, so the request is refused before anything is read from the store or written to it.

That covers every string and member name in a request body, wherever it stands (an entity, a subscription, a registration, a Snapshot, a hosted @context, a temporal evolution), every query parameter value, and a q or csf string operand that spells the character \u0000. The error says where the character is, as the JSON Pointer of the string or member in a body, or as the name of the query parameter.

In a batch only the entity that holds it is reported, and the rest are written. When a context source sends an entity or notification that holds it, Andromeda drops it with a warning, as it does with other data it can't hold. Other control characters are stored as written.

The NGSI-LD Null​

The NGSI-LD Null, urn:ngsi-ld:null, is an instruction to delete, never data. The rules below follow from that.

A write that sends a whole payload refuses it wherever it appears. Those writes are create, append, replace entity, replace attribute, batch create, batch update, and upsert in replace mode. It also means you can't store the string urn:ngsi-ld:null as an ordinary value.

An upsert in update mode (options=update) accepts the Null for an entity that already exists, because for that entity the upsert runs Update Attributes. For an entity the upsert creates, the Null is refused at any depth, because that path is Create Entity. Only that entity is refused, and the refusal shows in the 207. The rest of the batch is written.

Under a key of a JSON object inside a Property value, only a merge accepts the Null, and there it deletes that key. The object can be a structured value, a JSON literal inside one, or a JsonProperty json. Every other write refuses it there, the Temporal API included. In a temporal write the Null is allowed only as the value of a deleted instance (see Temporal evolution).

A first-level member set to the Null, as in "speed": "urn:ngsi-ld:null", removes the whole attribute with all its datasetId instances, in Merge Entity and Batch Entity Merge alike. To JSON-LD, ["urn:ngsi-ld:null"] and {"@id": "urn:ngsi-ld:null"} are the same Null, so they do the same. A reified {"type": "Property", "value": "urn:ngsi-ld:null"} without a datasetId deletes only the default instance.

The Null can't share a member with other values, in any operation. Next to them it could neither delete the member nor stand as data.

Under one language of a languageMap, the Null deletes that language in a merge. It is refused next to a string in the same language, as in {"en": ["Rue Cler", "urn:ngsi-ld:null"]}.

A datasetId can't be set to the Null, and an entity's expiresAt takes it only inside a fragment.