Edit this page

You use payload mapping to transform messages between your device’s native format and Ditto Protocol JSON.

TL;DR: Payload mapping transforms arbitrary payloads consumed via connections to Ditto Protocol messages and vice versa. Use built-in mappers or write custom JavaScript to handle any format.

Overview

Devices rarely send data in Ditto Protocol format. A device might send:

{"val": "23.42 °C", "ts": 1523946112727}

Or even binary:

0x08BD

Payload mapping bridges this gap by converting between device-native formats and the structured Ditto Protocol that Ditto requires.

Built-in mappers

Mapper Alias Inbound Outbound Description
Ditto Ditto Messages already in Ditto Protocol format
JavaScript JavaScript Custom JS scripts for arbitrary formats
Normalized Normalized   Transforms events to a normalized JSON view
ConnectionStatus ConnectionStatus   Updates a feature based on ttd/creation-time headers
RawMessage RawMessage Maps message command payloads directly
ImplicitThingCreation ImplicitThingCreation   Auto-creates things from incoming messages
UpdateTwinWithLiveResponse UpdateTwinWithLiveResponse   Patches twin data from live responses
CloudEvents CloudEvents Maps CloudEvent format to Ditto Protocol

Ditto mapper

The default mapper. Assumes messages are in Ditto Protocol JSON. No configuration required – use the alias Ditto directly.

JavaScript mapper

Transforms arbitrary payloads using custom JavaScript scripts executed in a sandboxed Rhino engine. See the JavaScript mapping engine section for details.

Options:

Option Required Description
incomingScript Yes Script for inbound messages
outgoingScript Yes Script for outbound messages
loadBytebufferJS No Load ByteBufferJS library (default: false)
loadLongJS No Load LongJS library (default: false)

Normalized mapper

Transforms created, modified, and deleted events to a normalized JSON structure. Other message types are dropped.

Options:

Option Default Description
fields all Comma-separated list of field selectors
includeDeletedFields false Track partial deletions in _deletedFields

Example input:

{
  "topic": "thing/id/things/twin/events/modified",
  "path": "/features/sensors/properties/temperature/value",
  "value": 42
}

Normalized output:

{
  "thingId": "thing:id",
  "features": {
    "sensors": {
      "properties": {
        "temperature": { "value": 42 }
      }
    }
  },
  "_context": {
    "topic": "thing/id/things/twin/events/modified",
    "path": "/features/sensors/properties/temperature/value",
    "value": 42
  }
}

For deleted events, the mapper includes a _deleted field with the deletion timestamp:

{
  "topic": "thing/id/things/twin/events/deleted",
  "headers": { "content-type": "application/json" },
  "path": "/",
  "value": null
}

Normalized output:

{
  "thingId": "thing:id",
  "_deleted": "2023-12-01T10:30:00Z",
  "_context": {
    "topic": "thing/id/things/twin/events/deleted",
    "path": "/",
    "value": null,
    "headers": {
      "content-type": "application/json"
    }
  }
}

The _deleted field contains the ISO-8601 timestamp when the thing was deleted. This field is only added for complete thing deletions.

When includeDeletedFields is enabled, partial deletions and merge-patch deletions are tracked in _deletedFields. The _deletedFields object mirrors the JSON structure of the deleted paths and stores ISO-8601 timestamps at the leaf nodes:

{
  "thingId": "thing:id",
  "_deletedFields": {
    "attributes": {
      "location": {
        "mountedOn": "2025-01-27T10:00:00Z"
      }
    },
    "features": {
      "myFeature": "2025-01-27T11:00:00Z"
    }
  }
}

ConnectionStatus mapper

Transforms ttd and creation-time headers (from Eclipse Hono device notifications) into a ModifyFeature command that updates a ConnectionStatus feature.

Typically used alongside another mapper: "payloadMapping": ["Ditto", "connectionStatus"]

Options:

Option Required Description
thingId Yes Thing ID (supports placeholders like {{ header:device_id }})
featureId No Feature ID (default: ConnectionStatus)

Example of a resulting ConnectionStatus feature:

{
  "thingId": "eclipse:ditto",
  "features": {
    "ConnectionStatus": {
      "definition": [ "org.eclipse.ditto:ConnectionStatus:1.0.0" ],
      "properties": {
        "status": {
          "readySince": "2019-10-29T14:16:18Z",
          "readyUntil": "2019-10-29T14:21:18Z"
        }
      }
    }
  }
}

Use the ConnectionStatus mapper alongside another mapper in a source configuration:

{
  "addresses": ["<source>"],
  "authorizationContext": ["ditto:inbound"],
  "payloadMapping": ["Ditto", "connectionStatus"]
}

RawMessage mapper

Maps message command/response payloads directly to/from the external message format. The encoding is determined by the content type.

For incoming messages, the mapper wraps the payload in a message command envelope. For outgoing messages, the mapper extracts the "value" field for publishing.

Messages with content-type: application/vnd.eclipse.ditto+json fall through to the Ditto mapper.

Options:

Option Default Description
outgoingContentType text/plain; charset=UTF-8 Fallback content type for outgoing messages
incomingMessageHeaders (see below) Headers for constructing the message envelope

Key incoming headers (all support placeholders):

Header Purpose Default
content-type Encoding of the payload {{ header:content-type \| fn:default('application/octet-stream') }}
ditto-message-subject Message subject (required for MQTT 3) {{ header:ditto-message-subject }}
ditto-message-thing-id Target thing ID (required for MQTT 3) {{ header:ditto-message-thing-id }}
ditto-message-direction TO (inbox) or FROM (outbox) TO
ditto-message-feature-id Feature ID (omit for thing-level messages) {{ header:ditto-message-feature-id }}
status Include for responses, omit for commands {{ header:status }}

The mapper maps between a feature message command response like:

{
  "topic": "org.eclipse.ditto/smartcoffee/things/live/messages/heatUp",
  "headers": { "content-type": "application/octet-stream" },
  "path": "/features/water-tank/inbox/messages/heatUp",
  "value": "AQIDBAUG",
  "status": 200
}

and an AMQP, MQTT 5, or Kafka message with payload of 6 bytes:

0x01 02 03 04 05 06

with headers:

content-type: application/octet-stream
status: 200
ditto-message-subject: heatUp
ditto-message-direction: TO
ditto-message-thing-id: org.eclipse.ditto:smartcoffee
ditto-message-feature-id: water-tank

Headers are lost for connection protocols without application headers such as MQTT 3.

Example configuration:

{
  "outgoingContentType": "application/octet-stream",
  "incomingMessageHeaders": {
    "content-type": "{{ header:content-type | fn:default('application/octet-stream') }}",
    "status": "{{ header:status }}",
    "ditto-message-subject": "{{ header:ditto-message-subject | fn:default('fallback-subject') }}",
    "ditto-message-direction": "TO",
    "ditto-message-thing-id": "{{ header:ditto-message-thing-id | fn:default('ns:fallback-thing') }}",
    "ditto-message-feature-id": "{{ header:ditto-message-feature-id }}"
  }
}

ImplicitThingCreation mapper

Automatically creates a thing when an incoming message arrives. The thing structure is defined in the thing option as a JSON template with placeholder support.

Options:

  • thing (required): The values of the thing that is created implicitly. It can contain fixed values or header placeholders (e.g. {{ header:device_id }}).
    • The following placeholders may be used inside the "thing" JSON:

      Placeholder Description
      {{ header:<header-name> }} Header value from the external message, e.g. from protocol headers
      {{ request:subjectId }} The first authenticated subjectId which did the request
      {{ time:now }} The current timestamp in ISO-8601 format as string in UTC timezone
      {{ time:now_epoch_millis }} The current timestamp in “milliseconds since epoch” formatted as string
    • The "thing" JSON may also include:

      • an inline policy: "_policy" containing the Policy JSON
      • a “copy policy from” statement: "_copyPolicyFrom" - see also create Thing alternatives
        • either including a policyId to copy from
        • or containing the link to a thing to copy the policy from in the form: {{ ref:things/<theThingId>/policyId }}
  • commandHeaders (optional, default: {"If-None-Match": "*"}): The Ditto headers to use for constructing the “create thing” command.
    • The following placeholders may be used:

      Placeholder Description
      {{ header:<header-name> }} Header value from the external message, e.g. from protocol headers
      {{ request:subjectId }} The first authenticated subjectId which did the request
      {{ time:now }} The current timestamp in ISO-8601 format as string in UTC timezone
      {{ time:now_epoch_millis }} The current timestamp in “milliseconds since epoch” formatted as string
  • allowPolicyLockout (optional, default: true): Whether it should be allowed to create policies without having WRITE permissions in the created policy for the subject which creates the policy.
{
  "thing": {
    "thingId": "{{ header:device_id }}",
    "attributes": {
      "CreatedBy": "ImplicitThingCreation"
    }
  },
  "commandHeaders": {
    "If-None-Match": "*",
    "correlation-id": "{{ header:correlation-id }}"
  },
  "allowPolicyLockout": true
}

UpdateTwinWithLiveResponse mapper

Creates a merge Thing command from a live retrieve response, patching the live data into the twin.

Options:

  • dittoHeadersForMerge (optional): The Ditto headers to use for constructing the “merge thing” command, may for example add a condition to apply in order to update the twin (default: "response-required": false, "if-match": "*").
    • The following placeholders may be used:

      Placeholder Description
      {{ header:<header-name> }} Header value from the external message, e.g. from protocol headers
      {{ request:subjectId }} The first authenticated subjectId which did the request
      {{ time:now }} The current timestamp in ISO-8601 format as string in UTC timezone
      {{ time:now_epoch_millis }} The current timestamp in “milliseconds since epoch” formatted as string

Example configuration:

{
  "dittoHeadersForMerge": {
    "if-match": "*",
    "response-required": false,
    "put-metadata": [
      {"key":"*/updated-by","value":"{{ request:subjectId }}"},
      {"key":"*/updated-via","value":"device-live-response"},
      {"key":"*/update-hint","value":"{{ header:some-custom-hint }}"},
      {"key":"*/updated-at","value":"{{ time:now }}"}
    ]
  }
}

CloudEvents mapper

Maps incoming CloudEvents to Ditto Protocol. Supports both Binary and Structured CloudEvents.

Note: The mapper supports incoming Structured CloudEvents messages with content-type:application/cloudevents+json and Binary CloudEvents messages with content-type:application/vnd.eclipse.ditto+json.

CloudEvents examples

Incoming messages need to have the mandatory CloudEvents fields. For example, a Binary CloudEvent for Ditto would look like this:

  headers:
      ce-specversion:1.0
      ce-id:some-id
      ce-type:some-type
      ce-source:generic-producer
      content-type:application/vnd.eclipse.ditto+json
{
  "topic": "my.sensors/sensor01/things/twin/commands/modify",
  "path": "/",
  "value": {
    "thingId": "my.sensors:sensor01",
    "policyId": "my.test:policy",
    "attributes": {
      "manufacturer": "Well known sensors producer",
      "serial number": "100",
      "location": "Ground floor"
    },
    "features": {
      "measurements": {
        "properties": {
          "temperature": 100,
          "humidity": 0
        }
      }
    }
  }
}

A Structured CloudEvent for Ditto would look like this:

headers:
  content-type:application/cloudevents+json
{
  "specversion": "1.0",
  "id": "3212e",
  "source": "http:somesite.com",
  "type": "com.site.com",
  "data": {
    "topic": "my.sensors/sensor01/things/twin/commands/modify",
    "path": "/",
    "value": {
      "thingId": "my.sensors:sensor01",
      "policyId": "my.test:policy",
      "attributes": {
        "manufacturer": "Well known sensors producer",
        "serial number": "100",
        "location": "Ground floor"
      },
      "features": {
        "measurements": {
          "properties": {
            "temperature": 100,
            "humidity": 0
          }
        }
      }
    }
  }
}

Using multiple mappers

Reference multiple mappers in a source’s payloadMapping array. Define custom mappers in mappingDefinitions:

{
  "name": "exampleConnection",
  "sources": [{
    "addresses": ["<source>"],
    "authorizationContext": ["ditto:inbound"],
    "payloadMapping": ["Ditto", "status"]
  }],
  "mappingDefinitions": {
    "status": {
      "mappingEngine": "ConnectionStatus",
      "options": {
        "thingId": "{{ header:device_id }}"
      }
    }
  }
}

Mapping conditions

You can add incomingConditions and outgoingConditions to control when a mapper executes. All conditions must evaluate to true for the mapping to run:

{
  "mappingDefinitions": {
    "status": {
      "mappingEngine": "ConnectionStatus",
      "incomingConditions": {
        "sampleCondition": "fn:filter(header:incoming-mapping-required,'eq','true')"
      },
      "outgoingConditions": {
        "sampleCondition": "fn:filter(header:outgoing-mapping-required,'eq','true')"
      },
      "options": {
        "thingId": "{{ header:device_id }}"
      }
    }
  }
}

JavaScript mapping engine

Ditto uses the Rhino JavaScript engine (version 1.7.14, ES6 flag enabled) with strict sandboxing for security.

Sandboxing constraints

  • No access to Java packages or classes
  • No file access, network calls, or exit/quit/print
  • Endless loops and deep recursion are terminated
  • Script file size is limited
  • No foreign JS library loading (unless included inline)

Check Rhino compatibility for supported ES6 features.

Helper libraries

You can load these libraries via specificConfig options:

Adding CommonJS modules

Configure CONNECTIVITY_MESSAGE_MAPPING_JS_COMMON_JS_MODULE_PATH to point to a directory containing CommonJS modules (for example, via a Docker volume mount):

CONNECTIVITY_MESSAGE_MAPPING_JS_COMMON_JS_MODULE_PATH=/opt/commonjs-modules/

Then use require() in your scripts:

var Pbf = require('pbf');

Helper functions

Ditto provides these functions under the Ditto scope:

// Builds a Ditto Protocol message from the passed parameters.
function buildDittoProtocolMsg(namespace, name, group, channel,
    criterion, action, path, dittoHeaders, value, status, extra) {
    const topic = buildTopic(namespace, name, group, channel, criterion, action);

    return {
        topic: topic,
        path: path,
        headers: dittoHeaders,
        value: value,
        status: status,
        extra: extra,
    };
}

// Builds a Ditto Protocol topic string.
function buildTopic(namespace, name, group, channel, criterion, action) {
    const topicChannel = 'none' === channel ? '' : '/' + channel;

    return namespace + "/" + name + "/" + group + topicChannel + "/" + criterion + "/" + action;
}

// Builds an external message from the passed parameters.
function buildExternalMsg(headers, textPayload, bytePayload, contentType) {

  return {
    headers: headers,
    textPayload: textPayload,
    bytePayload: bytePayload,
    contentType: contentType,
  };
}

// Transforms an ArrayBuffer to a String (unsigned 8-bit integers).
function arrayBufferToString(arrayBuffer) {

  return String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
}

// Transforms a String to an ArrayBuffer (unsigned 8-bit integers).
function stringToArrayBuffer(string) {

  let buf = new ArrayBuffer(string.length);
  let bufView = new Uint8Array(buf);
  for (let i=0, strLen=string.length; i<strLen; i++) {
    bufView[i] = string.charCodeAt(i);
  }
  return buf;
}

// Transforms an ArrayBuffer to a ByteBuffer (requires bytebuffer.js).
function asByteBuffer(arrayBuffer) {

  let byteBuffer = new ArrayBuffer(arrayBuffer.byteLength);
  new Uint8Array(byteBuffer).set(new Uint8Array(arrayBuffer));
  return dcodeIO.ByteBuffer.wrap(byteBuffer);
}

Mapping incoming messages

Implement mapToDittoProtocolMsg to convert external payloads to Ditto Protocol:

function mapToDittoProtocolMsg(
  headers,
  textPayload,
  bytePayload,
  contentType
) {

  // Insert/adapt your mapping logic here.
  // Use Ditto.buildDittoProtocolMsg to build Ditto Protocol messages from incoming payload.
  if (contentType === 'application/vnd.eclipse.ditto+json') {
    // Message is already in Ditto Protocol format -- use directly
    return JSON.parse(textPayload);
  } else if (contentType === 'application/octet-stream') {
    // Binary payload -- assume Ditto Protocol message (JSON)
    try {
      return JSON.parse(Ditto.arrayBufferToString(bytePayload));
    } catch (e) {
      // parsing failed (no JSON document); drop the message
      return null;
    }
  } else if (contentType === 'application/json') {
    let parsedJson = JSON.parse(textPayload);
    value = parsedJson.number1 + parsedJson['sub-field']; // access JSON keys with dashes using bracket notation
    // determine these variables from parsedJson and headers:
    let namespace = "";
    let name = "";
    let group = "things";
    let channel = "twin";
    let criterion = "commands";
    let action = "modify";
    let path = "/attributes";
    let dittoHeaders = {};
    let value = {
      "a": 1
    };
    return Ditto.buildDittoProtocolMsg(
      namespace,
      name,
      group,
      channel,
      criterion,
      action,
      path,
      dittoHeaders,
      value)
  }
  // no mapping logic matched; drop the message
  return null;
}

Return a single Ditto Protocol message, an array of messages, or null to drop the message.

For full access to the external message object, implement mapToDittoProtocolMsgWrapper instead:

function mapToDittoProtocolMsgWrapper(externalMsg) {
  let headers = externalMsg.headers;
  let textPayload = externalMsg.textPayload;
  let bytePayload = externalMsg.bytePayload;
  let contentType = externalMsg.contentType;
  return mapToDittoProtocolMsg(headers, textPayload, bytePayload, contentType);
}

Mapping outgoing messages

Implement mapFromDittoProtocolMsg to convert Ditto Protocol messages to external format:

function mapFromDittoProtocolMsg(namespace, name, group, channel,
    criterion, action, path, dittoHeaders, value, status, extra) {
  let headers = dittoHeaders;
  let textPayload = JSON.stringify(
    Ditto.buildDittoProtocolMsg(namespace, name, group, channel,
      criterion, action, path, dittoHeaders, value, status, extra)
  );
  return Ditto.buildExternalMsg(headers, textPayload, null,
    'application/vnd.eclipse.ditto+json');
}

For access to the full Ditto Protocol message (including revision), implement mapFromDittoProtocolMsgWrapper:

function mapFromDittoProtocolMsgWrapper(dittoProtocolMsg) {

  let topic = dittoProtocolMsg.topic;
  let splitTopic = topic.split("/");

  let namespace = splitTopic[0];
  let name = splitTopic[1];
  let group = splitTopic[2];

  let channel;
  let criterion;
  let action;
  if (hasChannel(group)) {
    channel = splitTopic[3];
    criterion = splitTopic[4];
    action = splitTopic[5];
  } else {
    channel = 'none';
    criterion = splitTopic[3];
    action = splitTopic[4];
  }

  let path = dittoProtocolMsg.path;
  let dittoHeaders = dittoProtocolMsg.headers;
  let value = dittoProtocolMsg.value;
  let status = dittoProtocolMsg.status;
  let extra = dittoProtocolMsg.extra;

  return mapFromDittoProtocolMsg(namespace, name, group, channel, criterion, action, path, dittoHeaders, value, status, extra);
}

Working with byte payloads

Use TypedArrays or DataViews to process binary data:

// TypedArray approach
let bytes = new Uint8Array(bytePayload);

// DataView approach (mixed types)
let view = new DataView(bytePayload);
view.getInt8(0);                       // 8-bit signed integer (byte) at offset 0
view.getUint16(1);                     // 16-bit unsigned integer (unsigned short) at offset 1
let temp = view.getInt16(0) / 100.0;   // 16-bit signed int at offset 0
let pressure = view.getInt16(2);       // 16-bit signed int at offset 2
let humidity = view.getUint8(4);       // 8-bit unsigned int at offset 4

Or use ByteBuffer.js (load with "loadBytebufferJS": "true"):

let byteBuf = Ditto.asByteBuffer(bytePayload);
let numberFromBytes = parseInt(byteBuf.toHex(), 16);

let base64encoded = byteBuf.toBase64();
let buf = dcodeIO.ByteBuffer.fromBase64(base64encoded);

buf.readInt(); // read a 32bit signed integer + advances the offset in the buffer
buf.readUTF8String(4); // read 4 characters of UTF-8 encoded string + advances the offset in the buffer
buf.remaining(); // gets the number of remaining readable bytes in the buffer

Check the ByteBuffer API documentation for the full list of operations.

JavaScript examples

Text payload example

Device sends JSON telemetry:

{"temp": "23.42 °C", "hum": 78, "pres": {"value": 760, "unit": "mmHg"}}

Mapping to update thing features:

function mapToDittoProtocolMsg(headers, textPayload, bytePayload, contentType) {
  if (contentType !== 'application/json') return null;

  let jsonData = JSON.parse(textPayload);
  let value = {
    temperature: { properties: { value: parseFloat(jsonData.temp.split(" ")[0]) } },
    pressure: { properties: { value: jsonData.pres.value } },
    humidity: { properties: { value: jsonData.hum } }
  };

  return Ditto.buildDittoProtocolMsg(
    'org.eclipse.ditto', headers['device_id'],
    'things', 'twin', 'commands', 'modify',
    '/features', headers, value
  );
}

Send this payload via Eclipse Hono’s MQTT adapter:

mosquitto_pub -u 'sensor1@DEFAULT_TENANT' -P hono-secret -t telemetry -m '{"temp": "23.42 °C","hum": 78,"pres": {"value": 760,"unit": "mmHg"}}'

The digital twin is updated by applying the script and extracting the relevant values from the textPayload.

Binary payload example

Device sends 5 bytes as hexadecimal 0x09EF03F72A:

  • the first 2 bytes 09 EF represent the temperature as 16-bit signed integer (not a float, to save space)
  • the next 2 bytes 03 F7 represent the pressure as 16-bit signed integer
  • the last byte 2A represents the humidity as 8-bit unsigned integer
function mapToDittoProtocolMsg(headers, textPayload, bytePayload, contentType) {
  if (contentType !== 'application/octet-stream') return null;

  let view = new DataView(bytePayload);
  let value = {
    temperature: { properties: { value: view.getInt16(0) / 100.0 } },
    pressure: { properties: { value: view.getInt16(2) } },
    humidity: { properties: { value: view.getUint8(4) } }
  };

  return Ditto.buildDittoProtocolMsg(
    'org.eclipse.ditto', headers['device_id'],
    'things', 'twin', 'commands', 'modify',
    '/features', headers, value
  );
}

Send this payload via Eclipse Hono’s HTTP adapter:

echo -e $((0x09EF03F72A)) | curl -i -X POST -u sensor1@DEFAULT_TENANT:hono-secret -H 'Content-Type: application/octet-stream' --data-binary @- http://127.0.0.1:8080/telemetry

The digital twin is updated by applying the script and extracting the relevant values from the bytePayload.

Custom Java mapper

For advanced use cases, implement a custom Java-based mapper by extending AbstractMessageMapper.

Extend AbstractMessageMapper to provide a custom mapper:

public final class FooMapper extends AbstractMessageMapper {

    private static final String MAPPER_ALIAS = "Foo";

    public FooMapper(ActorSystem actorSystem, Config config) {
        super(actorSystem, config);
    }

    private FooMapper(AbstractMessageMapper copyFromMapper) {
        super(copyFromMapper);
    }

    @Override
    public String getAlias() {
        return MAPPER_ALIAS;
    }

    @Override
    public boolean isConfigurationMandatory() {
        return false;
    }

    @Override
    public MessageMapper createNewMapperInstance() {
        return new FooMapper(this);
    }

    @Override
    public List<Adaptable> map(ExternalMessage externalMessage) {
        // TODO implement mapping inbound messages consumed via "sources" to DittoProtocol adaptables
        return null;
    }

    @Override
    public DittoHeaders getAdditionalInboundHeaders(ExternalMessage externalMessage) {
        return DittoHeaders.empty();
    }

    @Override
    public List<ExternalMessage> map(Adaptable adaptable) {
        // TODO implement mapping DittoProtocol adaptables to outbound messages published via "targets"
        return null;
    }

    @Override
    private void doConfigure(Connection connection, MappingConfig mappingConfig,
            MessageMapperConfiguration configuration) {
        // extract configuration if needed
    }
}

Key methods to implement:

  • List<Adaptable> map(ExternalMessage message) – inbound mapping (return empty list to drop)
  • List<ExternalMessage> map(Adaptable adaptable) – outbound mapping (return empty list to drop)
  • String getAlias() – unique mapper alias (must start with uppercase)

To deploy:

  1. Add the mapper JAR to the connectivity service classpath (extending Ditto)
  2. Register the alias in connectivity configuration
  3. Reference the alias in your connection’s mappingDefinitions

For a complete example, see the custom-ditto-java-payload-mapper project.

Further reading

Tags: connectivity