Edit this page

Conditional requests let you apply commands to Things only when specific conditions about the Thing’s current state are met.

TL;DR: Use the condition header with an RQL expression to make updates conditional on the Thing’s current state. You can also use If-Match/If-None-Match headers for ETag-based conditions, and live-channel-condition for automatic twin/live switching.

Overview

Ditto supports conditional requests as defined in RFC-7232 using If-Match and If-None-Match headers. Additionally, the condition header lets you specify conditions based on the current state of the persisted twin.

You can combine both header types in one request. When you do, Ditto evaluates the ETag header first.

Defining conditions

You define conditions using RQL expressions. A condition specifies that Ditto should apply the request only if the expression evaluates to true against the current twin state.

For example, to update an attribute only if the current value is not already 42:

PUT /api/2/things/org.eclipse.ditto:foo1/attributes/value?condition=ne(attributes/value,42)
42

You can reference any field in the Thing to build a condition. This is useful for timestamp-based updates where you only want to apply a change if the incoming value is newer than the stored one.

  • If the condition is met, Ditto updates the Thing and emits an event.
  • If the condition is not met, Ditto does not modify the Thing and emits no event/change notification.

Conditional requests work with HTTP API, WebSocket, Ditto Protocol, and the Ditto Java Client.

Permissions for conditions

You need READ permission on the resource referenced in the condition. Otherwise, the request fails.

Examples

The following examples assume this Thing state:

{
  "thingId": "org.eclipse.ditto:fancy-thing",
  "policyId": "org.eclipse.ditto:fancy-thing",
  "attributes": {
    "location": "kitchen"
  },
  "features": {
    "temperature": {
      "properties": {
        "value": 23.42,
        "unit": "Celcius",
        "lastModified": "2021-08-10T15:07:20.398Z"
      }
    }
  }
}

The goal: update the temperature value only if the incoming value is newer than the stored one, using the lastModified field.

HTTP API

Specify the condition as a query parameter:

curl -X PUT -H 'Content-Type: application/json' /api/2/things/org.eclipse.ditto:fancy-thing/features/temperature/properties/value?condition=gt(features/temperature/properties/lastModified,'2021-08-10T15:10:02.592Z') -d 19.26

Or as an HTTP header:

curl -X PUT -H 'Content-Type: application/json' -H 'condition: gt(features/temperature/properties/lastModified,"2021-08-10T15:10:02.592Z")' /api/2/things/org.eclipse.ditto:fancy-thing/features/temperature/properties/value -d 19.26

Ditto Protocol

{
  "topic": "org.eclipse.ditto/fancy-thing/things/twin/commands/modify",
  "headers": {
    "condition": "gt(features/temperature/properties/lastModified,2021-08-10T15:10:02.592Z)"
  },
  "path": "/features/temperature/properties/value",
  "value": 19.26
}

Ditto Java Client

final Option<String> option =
        Options.condition("gt(features/temperature/properties/lastModified,\"2021-08-10T15:10:02.592Z\")")

client.twin().forFeature(ThingId.of("org.eclipse.ditto:fancy-thing"), "temperature")
        .putProperty("value", 42, option)
        .whenComplete((ununsed, throwable) -> {
            if (throwable != null) {
                System.out.println("Property update was not successfull: " + throwable.getMessage());
            } else {
                System.out.println("Updating the property was successful.");
            }
        });

Live channel condition

Ditto supports automatic switching between twin and live channels based on a condition.

Define the condition with RQL. If it matches, Ditto retrieves data from the device itself:

GET .../things/{thingId}?live-channel-condition=eq(attributes/useLiveChannel,true)
GET .../things/{thingId}?live-channel-condition=lt(_modified,"2021-12-24T12:23:42Z")

Timeout strategy

The live-channel-timeout-strategy header controls what happens when the device does not respond within the timeout:

  • fail (default): return status code 408
  • use-twin: fall back to the persisted twin data

Response headers

The response includes two headers indicating which channel was used:

  • live-channel-condition-matched: true or false
  • channel: twin or live

You can use the UpdateTwinWithLiveResponse payload mapper to automatically update the digital twin with live response data.

Path-specific conditions

Since Ditto 3.8.0, you can apply different conditions to different parts of a merge operation using the merge-thing-patch-conditions header.

The header contains a JSON object where each key is a JSON pointer path (relative to the merge command’s path) and each value is an RQL expression. Paths without conditions are always applied.

  • If a path-specific condition is met, that part of the merge patch is applied.
  • If a path-specific condition is not met, that part is skipped.
  • Paths without conditions are always applied.

Permissions for path-specific conditions

You need READ permission on all resources referenced in the conditions.

Examples

Given this Thing state:

{
  "thingId": "org.eclipse.ditto:fancy-thing",
  "policyId": "org.eclipse.ditto:fancy-thing",
  "attributes": {
    "location": "kitchen",
    "manufacturer": "ACME Corp",
    "lastMaintenance": "2023-01-15T10:30:00Z"
  },
  "features": {
    "temperature": {
      "properties": {
        "value": 15,
        "unit": "celsius",
        "lastUpdated": "2023-01-20T14:30:00Z"
      }
    },
    "humidity": {
      "properties": {
        "value": 90,
        "unit": "percent",
        "lastUpdated": "2023-01-20T14:30:00Z"
      }
    },
    "status": {
      "properties": {
        "state": "active",
        "mode": "automatic"
      }
    }
  }
}

HTTP API

curl -X PATCH -H 'Content-Type: application/merge-patch+json' \
    -H 'merge-thing-patch-conditions: {"features/temperature/properties/value": "gt(features/temperature/properties/value,20)", "features/humidity/properties/value": "lt(features/humidity/properties/value,80)"}' \
    http://localhost:8080/api/2/things/org.eclipse.ditto:fancy-thing \
    -d '{"features": {"temperature": {"properties": {"value": 25}}, "humidity": {"properties": {"value": 60}}, "status": {"properties": {"state": "updated"}}}}'

In this example:

  • temperature is not updated (15 is not > 20)
  • humidity is not updated (90 is not < 80)
  • status is always updated (no condition)

The same call can also target the /features path with adjusted condition keys. Notice that conditions are still relative to the root of the Thing, only the key paths are adjusted:

curl -X PATCH -H 'Content-Type: application/merge-patch+json' \
    -H 'merge-thing-patch-conditions: {"temperature/properties/value": "gt(features/temperature/properties/value,20)", "humidity/properties/value": "lt(features/humidity/properties/value,80)"}' \
    http://localhost:8080/api/2/things/org.eclipse.ditto:fancy-thing/features \
    -d '{"temperature": {"properties": {"value": 25}}, "humidity": {"properties": {"value": 60}}, "status": {"properties": {"state": "updated"}}}'

Ditto Protocol

{
  "topic": "org.eclipse.ditto/fancy-thing/things/twin/commands/merge",
  "headers": {
    "merge-thing-patch-conditions": "{\"features/temperature/properties/value\":\"gt(features/temperature/properties/value,20)\",\"features/humidity/properties/value\":\"lt(features/humidity/properties/value,80)\"}"
  },
  "path": "/",
  "value": {
    "attributes": {
      "lastMaintenance": "2023-01-20T15:00:00Z"
    },
    "features": {
      "temperature": { "properties": { "value": 25 } },
      "humidity": { "properties": { "value": 60 } },
      "status": { "properties": { "state": "updated" } }
    }
  }
}

Ditto Java Client

Map<String, String> cond = new HashMap<>();
cond.put("features/temperature/properties/value", "gt(features/temperature/properties/value,20)");
cond.put("features/humidity/properties/value", "lt(features/humidity/properties/value,80)");

Option<Map<String, String>> condOption = Options.mergeThingPatchConditions(patchConditions);

client.twin().forId(ThingId.of("org.eclipse.ditto:fancy-thing"))
        .merge(JsonObject.newBuilder()
                .set("attributes", JsonObject.newBuilder()
                        .set("lastMaintenance", "2023-01-20T15:00:00Z")
                        .build())
                .set("features", JsonObject.newBuilder()
                        .set("temperature", JsonObject.newBuilder()
                                .set("properties", JsonObject.newBuilder()
                                        .set("value", 25).build()).build())
                        .set("humidity", JsonObject.newBuilder()
                                .set("properties", JsonObject.newBuilder()
                                        .set("value", 60).build()).build())
                        .set("status", JsonObject.newBuilder()
                                .set("properties", JsonObject.newBuilder()
                                        .set("state", "updated").build()).build())
                        .build())
                .build(), condOption)
        .whenComplete((unused, throwable) -> {
            if (throwable != null) {
                System.out.println("Merge was not successful: " + throwable.getMessage());
            } else {
                System.out.println("Merge was successful.");
            }
        });

Empty object removal configuration

Configuration option: MERGE_REMOVE_EMPTY_OBJECTS_AFTER_PATCH_CONDITION_FILTERING

Default: false (empty objects preserved for backward compatibility).

When enabled, Ditto recursively removes empty JSON objects created by condition filtering. This prevents unnecessary database operations when all parts of a merge patch are filtered out.

Example scenario – given this Thing state:

{
  "features": {
    "temp": { "properties": { "value": 15 } },
    "hum": { "properties": { "value": 70 } }
  }
}

Merge payload:

{
  "features": {
    "temp": { "properties": { "value": 25 } },
    "hum": { "properties": { "value": 60 } }
  }
}

Patch conditions (both evaluate to false):

{
  "features/temp/properties/value": "gt(features/temp/properties/value,30)",
  "features/hum/properties/value": "lt(features/hum/properties/value,50)"
}

Result with configuration disabled (default) – empty objects preserved, database operation still occurs:

{
  "features": {
    "temp": { "properties": {} },
    "hum": { "properties": {} }
  }
}

Result with configuration enabled – completely empty payload, database operation skipped entirely (no storage, no event emission, no new revision):

{}

For configuration details, see Merge operations configuration.

Further reading

Tags: protocol http rql