diff --git a/things/service/src/main/resources/things.conf b/things/service/src/main/resources/things.conf
index 8fa2234437..b4a618552b 100755
--- a/things/service/src/main/resources/things.conf
+++ b/things/service/src/main/resources/things.conf
@@ -285,7 +285,7 @@ ditto {
           # whether to enforce a thing whenever the "definition" of the thing is updated to a new/other WoT TM
           # needed follow-up would likely be https://github.com/eclipse-ditto/ditto/issues/1843
           #  - only with that we would e.g. update all feature definitions to the ones of the new thing definitions
-          enforce-thing-description-modification = false # needs to be false until #1843 is done
+          enforce-thing-description-modification = true
           enforce-thing-description-modification = ${?THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_TD_MODIFICATION}
 
           forbid-thing-description-deletion = true
@@ -315,7 +315,7 @@ ditto {
 
         feature {
           # whether to enforce a feature whenever the "definition"(s) of the feature is updated to a new/other WoT TM(s)
-          enforce-feature-description-modification = false # needs to be false until #1843 is done
+          enforce-feature-description-modification = true
           enforce-feature-description-modification = ${?THINGS_WOT_TM_MODEL_VALIDATION_FEATURE_ENFORCE_FD_MODIFICATION}
 
           forbid-feature-description-deletion = true
diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java
index d256f7ca6c..a4afdb9433 100644
--- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java
+++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/AbstractSingleDataSchemaBuilder.java
@@ -16,6 +16,7 @@
 
 import java.util.Collection;
 import java.util.Optional;
+import java.util.function.Consumer;
 
 import javax.annotation.Nullable;
 
@@ -184,6 +185,12 @@ public B setType(@Nullable final DataSchemaType type) {
         return myself;
     }
 
+    @Override
+    public B enhanceObjectBuilder(final Consumer<JsonObjectBuilder> builderConsumer) {
+        builderConsumer.accept(wrappedObjectBuilder);
+        return myself;
+    }
+
     protected <J> void putValue(final JsonFieldDefinition<J> definition, @Nullable final J value) {
         final Optional<JsonKey> keyOpt = definition.getPointer().getRoot();
         if (keyOpt.isPresent()) {
diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java
index 68b313b794..fb3dea12c0 100644
--- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java
+++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/MutablePropertyBuilder.java
@@ -13,6 +13,7 @@
 package org.eclipse.ditto.wot.model;
 
 import java.util.Collection;
+import java.util.function.Consumer;
 
 import javax.annotation.Nullable;
 
@@ -127,6 +128,12 @@ public Property.Builder setDefault(@Nullable final JsonValue defaultValue) {
         return myself;
     }
 
+    @Override
+    public Property.Builder enhanceObjectBuilder(final Consumer<JsonObjectBuilder> builderConsumer) {
+        builderConsumer.accept(wrappedObjectBuilder);
+        return myself;
+    }
+
     @Override
     public Property.Builder setType(@Nullable final DataSchemaType type) {
         if (type != null) {
diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java
index 514fd1c94d..8de9c2145f 100644
--- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java
+++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/SingleDataSchema.java
@@ -16,6 +16,7 @@
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Consumer;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
@@ -25,6 +26,7 @@
 import org.eclipse.ditto.json.JsonFactory;
 import org.eclipse.ditto.json.JsonFieldDefinition;
 import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.json.JsonObjectBuilder;
 import org.eclipse.ditto.json.JsonValue;
 
 /**
@@ -157,6 +159,8 @@ interface Builder<B extends Builder<B, S>, S extends SingleDataSchema> {
 
         B setDefault(@Nullable JsonValue defaultValue);
 
+        B enhanceObjectBuilder(Consumer<JsonObjectBuilder> builderConsumer);
+
         S build();
     }
 
diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java
index 0686c11553..cf5230e08e 100644
--- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java
+++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalFeatureValidation.java
@@ -266,7 +266,7 @@ private static CompletableFuture<Void> enforceFeaturePropertyOnlyDefinedProperti
                         )
                         .toList());
                 final FeatureProperties featureProperties = FeatureProperties.newBuilder()
-                        .setAll(propertyValue.asObject())
+                        .setAll(propertyValue.isObject() ? propertyValue.asObject() : JsonObject.empty())
                         .build();
                 return ensureOnlyDefinedProperties(featureThingModel,
                         propertiesInCategory,
@@ -305,6 +305,16 @@ private static CompletableFuture<Void> enforceFeaturePropertyValidateProperties(
     ) {
         if (isCategoryUpdate) {
             final String dittoCategory = propertyPath.getRoot().orElseThrow().toString();
+            if (!propertyValue.isObject()) {
+                final WotThingModelPayloadValidationException.Builder exceptionBuilder =
+                        WotThingModelPayloadValidationException
+                                .newBuilder("Could not update Feature property category " +
+                                        "<" + dittoCategory + "> as its value was not a JSON object");
+                return CompletableFuture.failedFuture(exceptionBuilder
+                        .dittoHeaders(context.dittoHeaders())
+                        .build());
+            }
+
             final List<Property> sameCategoryProperties = tdProperties.values().stream()
                     .filter(property ->
                             // gather all properties from the same category
diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java
index c86d953844..497bd6d0fa 100644
--- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java
+++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/InternalValidation.java
@@ -46,6 +46,7 @@
 final class InternalValidation {
 
     private static final JsonSchemaTools JSON_SCHEMA_TOOLS = new JsonSchemaTools();
+    private static final String PROPERTIES_PATH_PREFIX = "/properties/";
 
     private InternalValidation() {
         throw new AssertionError();
@@ -286,11 +287,11 @@ static Map<String, Property> extractRequiredTmProperties(final Properties tdProp
             final Map<String, Property> allRequiredProperties = new LinkedHashMap<>(tdProperties);
             tmOptionalElements.stream()
                     .map(TmOptionalElement::toString)
-                    .filter(el -> el.startsWith("/properties/"))
-                    .map(el -> el.replace("/properties/", ""))
+                    .filter(el -> el.startsWith(PROPERTIES_PATH_PREFIX))
+                    .map(el -> el.replace(PROPERTIES_PATH_PREFIX, ""))
                     .forEach(allRequiredProperties::remove);
             return allRequiredProperties;
-        }).orElseGet(LinkedHashMap::new);
+        }).orElse(tdProperties);
     }
 
     static boolean isTmPropertyRequired(final Property property,
@@ -299,8 +300,8 @@ static boolean isTmPropertyRequired(final Property property,
         return thingModel.getTmOptional()
                 .map(tmOptionalElements -> tmOptionalElements.stream()
                         .map(TmOptionalElement::toString)
-                        .filter(el -> el.startsWith("/properties/"))
-                        .map(el -> el.replace("/properties/", ""))
+                        .filter(el -> el.startsWith(PROPERTIES_PATH_PREFIX))
+                        .map(el -> el.replace(PROPERTIES_PATH_PREFIX, ""))
                         .noneMatch(el -> property.getPropertyName().equals(el))
                 ).orElse(false);
     }
@@ -495,6 +496,7 @@ private static Optional<PropertyWithCategory> findPropertyBasedOnPath(final Thin
         if (handleDittoCategory) {
             return dittoCategories.stream()
                     .filter(category -> propertyPath.getRoot().orElseThrow().toString().equals(category))
+                    .filter(category -> propertyPath.getLevelCount() > 1)
                     .map(category ->
                             tdProperties.getProperty(propertyPath.get(1).orElseThrow().toString())
                                     .filter(p -> determineDittoCategory(thingModel, p)
diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java
index 5885cffd12..4bd6d94198 100644
--- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java
+++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/FeatureValidationConfig.java
@@ -98,7 +98,7 @@ public interface FeatureValidationConfig {
      */
     enum ConfigValue implements KnownConfigValue {
 
-        ENFORCE_FEATURE_DESCRIPTION_MODIFICATION("enforce-feature-description-modification", false),
+        ENFORCE_FEATURE_DESCRIPTION_MODIFICATION("enforce-feature-description-modification", true),
 
         FORBID_FEATURE_DESCRIPTION_DELETION("forbid-feature-description-deletion", true),
 
diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java
index 139c3fdb30..7cb8ea067c 100644
--- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java
+++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/ThingValidationConfig.java
@@ -75,7 +75,7 @@ public interface ThingValidationConfig {
      */
     enum ConfigValue implements KnownConfigValue {
 
-        ENFORCE_THING_DESCRIPTION_MODIFICATION("enforce-thing-description-modification", false),
+        ENFORCE_THING_DESCRIPTION_MODIFICATION("enforce-thing-description-modification", true),
 
         FORBID_THING_DESCRIPTION_DELETION("forbid-thing-description-deletion", true),
 
diff --git a/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java
new file mode 100644
index 0000000000..c9275709f7
--- /dev/null
+++ b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationFeatureLevelTest.java
@@ -0,0 +1,867 @@
+/*
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.ditto.wot.validation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+import org.eclipse.ditto.base.model.headers.DittoHeaders;
+import org.eclipse.ditto.json.JsonArray;
+import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.json.JsonPointer;
+import org.eclipse.ditto.json.JsonValue;
+import org.eclipse.ditto.things.model.Feature;
+import org.eclipse.ditto.things.model.FeatureProperties;
+import org.eclipse.ditto.things.model.Features;
+import org.eclipse.ditto.wot.model.Action;
+import org.eclipse.ditto.wot.model.Actions;
+import org.eclipse.ditto.wot.model.AtContext;
+import org.eclipse.ditto.wot.model.BaseLink;
+import org.eclipse.ditto.wot.model.Event;
+import org.eclipse.ditto.wot.model.Events;
+import org.eclipse.ditto.wot.model.Links;
+import org.eclipse.ditto.wot.model.Properties;
+import org.eclipse.ditto.wot.model.Property;
+import org.eclipse.ditto.wot.model.SingleAtContext;
+import org.eclipse.ditto.wot.model.SingleDataSchema;
+import org.eclipse.ditto.wot.model.SingleUriAtContext;
+import org.eclipse.ditto.wot.model.ThingModel;
+import org.eclipse.ditto.wot.model.TmOptional;
+import org.eclipse.ditto.wot.model.TmOptionalElement;
+import org.eclipse.ditto.wot.validation.config.FeatureValidationConfig;
+import org.eclipse.ditto.wot.validation.config.TmValidationConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Provides unit tests for testing the "feature" related functionality of {@link WotThingModelValidation}.
+ */
+public final class WotThingModelValidationFeatureLevelTest {
+
+    private static final String DITTO_CONTEXT_PREFIX = "ditto";
+    private static final String CATEGORY_CONFIG = "config";
+
+    private static final String PROP_SOME_BOOL = "someBool";
+    private static final String PROP_SOME_INT = "someInt";
+    private static final JsonPointer PROP_PATH_SOME_INT = JsonPointer.of(CATEGORY_CONFIG + "/" + PROP_SOME_INT);
+    private static final String PROP_SOME_NUMBER = "someNumber";
+    private static final String PROP_SOME_STRING = "someString";
+    private static final JsonPointer PROP_PATH_SOME_STRING = JsonPointer.of(CATEGORY_CONFIG + "/" + PROP_SOME_STRING);
+    private static final String PROP_SOME_ARRAY_STRINGS = "someArray_strings";
+    private static final String PROP_SOME_OBJECT = "someObject";
+
+    private static final JsonObject PROP_KNOWN_SOME_OBJECT = JsonObject.newBuilder()
+            .set(PROP_SOME_BOOL, false)
+            .set(PROP_SOME_INT, 3)
+            .set(PROP_SOME_STRING, "helo")
+            .build();
+
+    private static final Properties KNOWN_PROPERTIES = Properties.from(List.of(
+            Property.newBuilder(PROP_SOME_BOOL)
+                    .setSchema(SingleDataSchema.newBooleanSchemaBuilder().build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_INT)
+                    .setSchema(SingleDataSchema.newIntegerSchemaBuilder().build())
+                    .set(DITTO_CONTEXT_PREFIX + ":category", CATEGORY_CONFIG)
+                    .build(),
+            Property.newBuilder(PROP_SOME_NUMBER)
+                    .setSchema(SingleDataSchema.newNumberSchemaBuilder().build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_STRING)
+                    .setSchema(SingleDataSchema.newStringSchemaBuilder().build())
+                    .set(DITTO_CONTEXT_PREFIX + ":category", CATEGORY_CONFIG)
+                    .build(),
+            Property.newBuilder(PROP_SOME_ARRAY_STRINGS)
+                    .setSchema(SingleDataSchema.newArraySchemaBuilder()
+                            .setItems(SingleDataSchema.newStringSchemaBuilder().build())
+                            .build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_OBJECT)
+                    .setSchema(SingleDataSchema.newObjectSchemaBuilder()
+                            .setProperties(Map.of(
+                                    PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                    PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                    PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                            ))
+                            .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                            .enhanceObjectBuilder(builder -> builder.set("additionalProperties", false))
+                            .build())
+                    .build()
+    ));
+
+    private static final String ACTION_PROCESS_BOOL = "processBool";
+    private static final String ACTION_PROCESS_OBJECT = "processObject";
+
+    private static final Actions KNOWN_ACTIONS = Actions.from(List.of(
+            Action.newBuilder(ACTION_PROCESS_BOOL)
+                    .setInput(SingleDataSchema.newBooleanSchemaBuilder().build())
+                    .setOutput(SingleDataSchema.newBooleanSchemaBuilder().build())
+                    .build(),
+            Action.newBuilder(ACTION_PROCESS_OBJECT)
+                    .setInput(SingleDataSchema.newObjectSchemaBuilder()
+                            .setProperties(Map.of(
+                                    PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                    PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                    PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                            ))
+                            .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                            .build()
+                    )
+                    .setOutput(SingleDataSchema.newObjectSchemaBuilder()
+                            .setProperties(Map.of(
+                                    PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                    PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                    PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                            ))
+                            .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                            .build())
+                    .build()
+    ));
+
+    private static final String EVENT_EMIT_INT = "emitInt";
+    private static final String EVENT_EMIT_ARRAY = "emitArray_objects";
+
+    private static final Events KNOWN_EVENTS = Events.from(List.of(
+            Event.newBuilder(EVENT_EMIT_INT)
+                    .setData(SingleDataSchema.newIntegerSchemaBuilder().build())
+                    .build(),
+            Event.newBuilder(EVENT_EMIT_ARRAY)
+                    .setData(SingleDataSchema.newArraySchemaBuilder()
+                            .setItems(SingleDataSchema.newObjectSchemaBuilder()
+                                    .setProperties(Map.of(
+                                            PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                            PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                            PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                                    ))
+                                    .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                                    .build())
+                            .build()
+                    )
+                    .build()
+    ));
+
+    private static final String KNOWN_FEATURE_ID = "known-feature";
+    private static final String KNOWN_FEATURE_ID_2 = "known-feature-2";
+
+    private static final ThingModel KNOWN_THING_LEVEL_TM_WITH_SUBMODELS = ThingModel.newBuilder()
+            .setLinks(Links.of(List.of(
+                    BaseLink.newLinkBuilder()
+                            .setType("tm:submodel")
+                            .build()
+            )))
+            .build();
+
+    private static final ThingModel KNOWN_FEATURE_LEVEL_TM = ThingModel.newBuilder()
+            .setAtContext(AtContext.newMultipleAtContext(List.of(
+                    SingleAtContext.newSinglePrefixedAtContext(DITTO_CONTEXT_PREFIX,
+                            SingleUriAtContext.DITTO_WOT_EXTENSION)
+            )))
+            .setProperties(KNOWN_PROPERTIES)
+            .setTmOptional(TmOptional.of(List.of(
+                    TmOptionalElement.of("/properties/" + PROP_SOME_ARRAY_STRINGS),
+                    TmOptionalElement.of("/properties/" + PROP_SOME_OBJECT)
+            )))
+            .setActions(KNOWN_ACTIONS)
+            .setEvents(KNOWN_EVENTS)
+            .build();
+
+    private static final FeatureProperties KNOWN_FEATURE_PROPERTIES = FeatureProperties.newBuilder()
+            .set(PROP_SOME_BOOL, true)
+            .set(PROP_PATH_SOME_INT, 42)
+            .set(PROP_SOME_NUMBER, 42.23)
+            .set(PROP_PATH_SOME_STRING, "some")
+            .build();
+
+
+    private WotThingModelValidation sut;
+
+    @Before
+    public void setUp() {
+        final TmValidationConfig validationConfig = mock(TmValidationConfig.class);
+        when(validationConfig.isEnabled()).thenReturn(true);
+
+        final FeatureValidationConfig featureValidationConfig = mock(FeatureValidationConfig.class);
+        when(featureValidationConfig.isEnforcePresenceOfModeledFeatures()).thenReturn(true);
+        when(featureValidationConfig.isForbidNonModeledFeatures()).thenReturn(true);
+        when(featureValidationConfig.isEnforceProperties()).thenReturn(true);
+        when(featureValidationConfig.isEnforceDesiredProperties()).thenReturn(true);
+        when(featureValidationConfig.isForbidNonModeledProperties()).thenReturn(true);
+        when(featureValidationConfig.isForbidNonModeledDesiredProperties()).thenReturn(true);
+        when(featureValidationConfig.isEnforceInboxMessagesInput()).thenReturn(true);
+        when(featureValidationConfig.isEnforceInboxMessagesOutput()).thenReturn(true);
+        when(featureValidationConfig.isEnforceOutboxMessages()).thenReturn(true);
+        when(featureValidationConfig.isForbidNonModeledInboxMessages()).thenReturn(true);
+        when(featureValidationConfig.isForbidNonModeledOutboxMessages()).thenReturn(true);
+        when(validationConfig.getFeatureValidationConfig()).thenReturn(featureValidationConfig);
+
+        sut = WotThingModelValidation.of(validationConfig);
+    }
+
+    @Test
+    public void validateFeaturesDeletionSucceedsWithThingLevelModelNotHavingSubmodels() {
+        internalCheckFail(false, sut.validateThingScopedDeletion(ThingModel.newBuilder().build(),
+                Map.of(),
+                JsonPointer.of("features"),
+                provideValidationContext()
+        ));
+    }
+
+    @Test
+    public void validateFeaturesDeletionFailsWithThingLevelModelHavingSubmodels() {
+        internalCheckFail(true, sut.validateThingScopedDeletion(KNOWN_THING_LEVEL_TM_WITH_SUBMODELS,
+                Map.of(KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM),
+                JsonPointer.of("features"),
+                provideValidationContext()
+        ));
+    }
+
+    @Test
+    public void validateFeaturesPresenceSucceedsWhenAllModeledFeaturesArePresent() {
+        checkValidateFeaturesPresence(Features.newBuilder()
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID)
+                                .build())
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID_2)
+                                .build())
+                        .build(),
+                false
+        );
+    }
+
+    @Test
+    public void validateFeaturesPresenceFailsWhenNotAllModeledFeaturesArePresent() {
+        checkValidateFeaturesPresence(Features.newBuilder()
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID)
+                                .build())
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturesPresenceFailsWhenNonModeledFeaturesAreProvided() {
+        checkValidateFeaturesPresence(Features.newBuilder()
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID)
+                                .build())
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID_2)
+                                .build())
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId("unknown-feature")
+                                .build())
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturesPropertiesSucceeds() {
+        checkValidateFeaturesProperties(Features.newBuilder()
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID)
+                                .build())
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID_2)
+                                .build())
+                        .build(),
+                false
+        );
+    }
+
+    @Test
+    public void validateFeaturesPropertiesFailsWhenMissingRequiredProperty() {
+        checkValidateFeaturesProperties(Features.newBuilder()
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().remove(PROP_SOME_BOOL).build())
+                                .withId(KNOWN_FEATURE_ID)
+                                .build())
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID_2)
+                                .build())
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturesPropertiesFailsWhenPropertyHasWrongDatatype() {
+        checkValidateFeaturesProperties(Features.newBuilder()
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().set(PROP_SOME_BOOL, "not a bool").build())
+                                .withId(KNOWN_FEATURE_ID)
+                                .build())
+                        .set(Feature.newBuilder()
+                                .properties(KNOWN_FEATURE_PROPERTIES)
+                                .withId(KNOWN_FEATURE_ID_2)
+                                .build())
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturePresenceSucceeds() {
+        checkValidateFeaturePresence(Feature.newBuilder()
+                        .properties(KNOWN_FEATURE_PROPERTIES)
+                        .withId(KNOWN_FEATURE_ID)
+                        .build(),
+                false
+        );
+    }
+
+    @Test
+    public void validateFeaturePresenceFails() {
+        checkValidateFeaturePresence(Feature.newBuilder()
+                        .properties(KNOWN_FEATURE_PROPERTIES)
+                        .withId("unknown-id")
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeatureSucceeds() {
+        checkValidateFeature(Feature.newBuilder()
+                        .properties(KNOWN_FEATURE_PROPERTIES)
+                        .withId(KNOWN_FEATURE_ID)
+                        .build(),
+                false
+        );
+    }
+
+    @Test
+    public void validateFeatureSucceedsWithOptionalDesiredPropertyPresent() {
+        checkValidateFeature(Feature.newBuilder()
+                        .properties(KNOWN_FEATURE_PROPERTIES)
+                        .desiredProperties(FeatureProperties.newBuilder()
+                                .set(PROP_PATH_SOME_INT, 42)
+                                .build()
+                        )
+                        .withId(KNOWN_FEATURE_ID)
+                        .build(),
+                false
+        );
+    }
+
+    @Test
+    public void validateFeatureFailsWhenMissingRequiredProperty() {
+        checkValidateFeature(Feature.newBuilder()
+                        .properties(KNOWN_FEATURE_PROPERTIES.toBuilder().remove(PROP_PATH_SOME_STRING).build())
+                        .withId(KNOWN_FEATURE_ID)
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeatureFailsWithOptionalDesiredPropertyHavingWrongDatatype() {
+        checkValidateFeature(Feature.newBuilder()
+                        .properties(KNOWN_FEATURE_PROPERTIES)
+                        .desiredProperties(FeatureProperties.newBuilder()
+                                .set(PROP_PATH_SOME_INT, "not an int")
+                                .build()
+                        )
+                        .withId(KNOWN_FEATURE_ID)
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturePropertySucceedsForBooleanProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_BOOL, false, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateFeaturePropertiesSucceedsForBooleanProperty() {
+        checkValidateFeatureProperties(PROP_SOME_BOOL, false, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForBooleanProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_BOOL, false, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForBooleanProperty() {
+        checkValidateFeatureProperties(PROP_SOME_BOOL, false, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateFeaturePropertySucceedsForIntegerProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_INT, false, JsonValue.of(42), false);
+    }
+
+    @Test
+    public void validateFeaturePropertiesSucceedsForIntegerProperty() {
+        checkValidateFeatureProperties(PROP_PATH_SOME_INT, false, JsonValue.of(42), false);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForIntegerProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_INT, false, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForIntegerProperty() {
+        checkValidateFeatureProperties(PROP_PATH_SOME_INT, false, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateFeaturePropertySucceedsForNumberProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_NUMBER, false, JsonValue.of(42.23), false);
+    }
+
+    @Test
+    public void validateFeaturePropertiesSucceedsForNumberProperty() {
+        checkValidateFeatureProperties(PROP_SOME_NUMBER, false, JsonValue.of(42.23), false);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForNumberProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_NUMBER, false, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForNumberProperty() {
+        checkValidateFeatureProperties(PROP_SOME_NUMBER, false, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateFeaturePropertySucceedsForStringProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_STRING, false, JsonValue.of("some"), false);
+    }
+
+    @Test
+    public void validateFeaturePropertiesSucceedsForStringProperty() {
+        checkValidateFeatureProperties(PROP_PATH_SOME_STRING, false, JsonValue.of("some"), false);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForStringProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_PATH_SOME_STRING, false, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForStringProperty() {
+        checkValidateFeatureProperties(PROP_PATH_SOME_STRING, false, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateFeaturePropertySucceedsForArraysStringProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false,
+                JsonArray.of("some", "string", "arr"),
+                false);
+    }
+
+    @Test
+    public void validateFeaturePropertiesSucceedsForArraysStringProperty() {
+        checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, JsonArray.of("some", "string", "arr"), false);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForArraysStringProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false, JsonArray.of(false, true, false),
+                true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForArraysStringProperty() {
+        checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false, JsonArray.of(false, true, false), true);
+    }
+
+    @Test
+    public void validateFeaturePropertySucceedsForObjectProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_OBJECT, false, PROP_KNOWN_SOME_OBJECT, false);
+    }
+
+    @Test
+    public void validateFeaturePropertySucceedsForCategoryUpdateContainingAllRequiredProperties() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonObject.newBuilder()
+                        .set(PROP_SOME_INT, 44)
+                        .set(PROP_SOME_STRING, "some")
+                        .build(),
+                false
+        );
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForCategoryUpdateMissingRequiredProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonObject.newBuilder()
+                        .set(PROP_SOME_INT, 44)
+                        .build(),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForCategoryUpdateNotBeingObject() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, CATEGORY_CONFIG, false, JsonValue.of("not a category object"),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturePropertiesSucceedsForObjectProperty() {
+        checkValidateFeatureProperties(PROP_SOME_OBJECT, false, PROP_KNOWN_SOME_OBJECT, false);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForObjectProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_OBJECT, false, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForObjectProperty() {
+        checkValidateFeatureProperties(PROP_SOME_OBJECT, false, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForObjectAttributeMissingRequiredField() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_PATH_SOME_STRING).build(), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForObjectAttributeMissingRequiredField() {
+        checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_PATH_SOME_STRING).build(), true);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForObjectAttributeAdditionalNotSpecifiedField() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, PROP_SOME_ARRAY_STRINGS, false,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForObjectAttributeAdditionalNotSpecifiedField() {
+        checkValidateFeatureProperties(PROP_SOME_ARRAY_STRINGS, false,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForNonModeledProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, "newStuff", false, JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForNonModeledProperties() {
+        checkValidateFeatureProperties("newStuff", false, JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateFeaturePropertyFailsForNonModeledDesiredProperty() {
+        checkValidateFeatureProperty(KNOWN_FEATURE_ID, "newStuff", true, JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateFeaturePropertiesFailsForNonModeledDesiredProperties() {
+        checkValidateFeatureProperties("newStuff", true, JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateFeaturePropertyDeletionSucceedsForOptionalArrayProperty() {
+        checkValidateFeaturePropertyDeletion(
+                JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/" + PROP_SOME_ARRAY_STRINGS),
+                false
+        );
+    }
+
+    @Test
+    public void validateFeaturePropertyDeletionFailsForFeatureContainingRequiredProperties() {
+        checkValidateFeaturePropertyDeletion(
+                JsonPointer.of("features/" + KNOWN_FEATURE_ID),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturePropertyDeletionFailsForFeaturePropertiesContainingRequiredProperties() {
+        checkValidateFeaturePropertyDeletion(
+                JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/"),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeaturePropertyDeletionFailsForFeaturePropertiesCategoryContainingRequiredProperties() {
+        checkValidateFeaturePropertyDeletion(
+                JsonPointer.of("features/" + KNOWN_FEATURE_ID + "/properties/" + CATEGORY_CONFIG),
+                true
+        );
+    }
+
+    @Test
+    public void validateFeatureActionBoolInputSucceeds() {
+        checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateFeatureActionBoolInputFailsWrongDatatype() {
+        checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true);
+    }
+
+    @Test
+    public void validateFeatureActionObjectInputSucceeds() {
+        checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_SOME_BOOL, false)
+                .set(PROP_SOME_INT, 23)
+                .set(PROP_SOME_STRING, "yes!")
+                .build(), false);
+    }
+
+    @Test
+    public void validateFeatureActionObjectInputFailsWrongDatatype() {
+        checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonArray.empty(), true);
+    }
+
+    @Test
+    public void validateFeatureActionObjectInputFailsMissingRequiredField() {
+        checkValidateFeatureActionInput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_PATH_SOME_STRING, "yes!")
+                .build(), true);
+    }
+
+    @Test
+    public void validateNonModeledFeatureActionInputFails() {
+        checkValidateFeatureActionInput(KNOWN_FEATURE_ID, "someNewAction", JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateFeatureActionBoolOutputSucceeds() {
+        checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateFeatureActionBoolOutputFailsWrongDatatype() {
+        checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true);
+    }
+
+    @Test
+    public void validateFeatureActionObjectOutputSucceeds() {
+        checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_SOME_BOOL, false)
+                .set(PROP_SOME_INT, 23)
+                .set(PROP_SOME_STRING, "yes!")
+                .build(), false);
+    }
+
+    @Test
+    public void validateFeatureActionObjectOutputFailsWrongDatatype() {
+        checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonArray.empty(), true);
+    }
+
+    @Test
+    public void validateFeatureActionObjectOutputFailsMissingRequiredField() {
+        checkValidateFeatureActionOutput(KNOWN_FEATURE_ID, ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_PATH_SOME_STRING, "yes!")
+                .build(), true);
+    }
+
+    @Test
+    public void validateFeatureEventIntDataSucceeds() {
+        checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_INT, JsonValue.of(33), false);
+    }
+
+    @Test
+    public void validateFeatureEventBoolDataFailsWrongDatatype() {
+        checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_INT, JsonValue.of("oh no"), true);
+    }
+
+    @Test
+    public void validateFeatureEventArrayDataSucceeds() {
+        checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonArray.newBuilder()
+                .add(JsonObject.newBuilder()
+                        .set(PROP_SOME_BOOL, false)
+                        .set(PROP_SOME_INT, 23)
+                        .set(PROP_SOME_STRING, "yes!")
+                        .build()
+                ).build(), false);
+    }
+
+    @Test
+    public void validateFeatureEventArrayDataFailsWrongDatatype() {
+        checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonObject.empty(), true);
+    }
+
+    @Test
+    public void validateFeatureEventArrayDataFailsMissingRequiredFieldInsideObject() {
+        checkValidateFeatureEventData(KNOWN_FEATURE_ID, EVENT_EMIT_ARRAY, JsonArray.newBuilder()
+                .add(JsonObject.newBuilder()
+                        .set(PROP_PATH_SOME_INT, 23)
+                        .set(PROP_PATH_SOME_STRING, "yes!")
+                        .build()
+                ).build(), true);
+    }
+
+    @Test
+    public void validateNonModeledFeatureEventDataFails() {
+        checkValidateFeatureEventData(KNOWN_FEATURE_ID, "someNewEvent", JsonValue.of(true), true);
+    }
+
+    private void checkValidateFeaturesPresence(@Nullable final Features features, final boolean mustFail) {
+        internalCheckFail(mustFail, sut.validateFeaturesPresence(Map.of(
+                        KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM,
+                        KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM
+                ),
+                features,
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeaturesProperties(@Nullable final Features features, final boolean mustFail) {
+        internalCheckFail(mustFail, sut.validateFeaturesProperties(Map.of(
+                        KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM,
+                        KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM
+                ),
+                features,
+                JsonPointer.of("features"),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeaturePresence(final Feature feature, final boolean mustFail) {
+        internalCheckFail(mustFail, sut.validateFeaturePresence(Map.of(
+                        KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM,
+                        KNOWN_FEATURE_ID_2, KNOWN_FEATURE_LEVEL_TM
+                ),
+                feature,
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeature(final Feature feature, final boolean mustFail) {
+        internalCheckFail(mustFail, sut.validateFeature(KNOWN_FEATURE_LEVEL_TM,
+                feature,
+                JsonPointer.of("features/" + feature.getId()),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeatureProperty(final String featureId, final CharSequence propertyPath,
+            final boolean desiredProperty,
+            final JsonValue propertyValue,
+            final boolean mustFail
+    ) {
+        final JsonPointer propertyPointer = JsonPointer.of(propertyPath);
+        internalCheckFail(mustFail, sut.validateFeatureProperty(KNOWN_FEATURE_LEVEL_TM,
+                KNOWN_FEATURE_ID,
+                propertyPointer,
+                propertyValue,
+                desiredProperty,
+                JsonPointer.of("features/" + featureId + "/properties").append(propertyPointer),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeatureProperties(final CharSequence propertyPath, final boolean desiredProperties,
+            final JsonValue propertyValue,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateFeatureProperties(KNOWN_FEATURE_LEVEL_TM,
+                KNOWN_FEATURE_ID,
+                KNOWN_FEATURE_PROPERTIES.toBuilder()
+                        .set(propertyPath, propertyValue)
+                        .build(),
+                desiredProperties,
+                JsonPointer.of("attributes"),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeaturePropertyDeletion(final JsonPointer resourcePath, final boolean mustFail) {
+        internalCheckFail(mustFail, sut.validateFeatureScopedDeletion(Map.of(KNOWN_FEATURE_ID, KNOWN_FEATURE_LEVEL_TM),
+                KNOWN_FEATURE_LEVEL_TM,
+                KNOWN_FEATURE_ID,
+                resourcePath,
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeatureActionInput(final String featureId, final String actionName,
+            @Nullable final JsonValue inputData,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateFeatureActionInput(KNOWN_FEATURE_LEVEL_TM,
+                featureId,
+                actionName,
+                inputData,
+                JsonPointer.of("features/" + featureId + "/inbox/messages/" + actionName),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeatureActionOutput(final String featureId, final String actionName,
+            @Nullable final JsonValue outputData,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateFeatureActionOutput(KNOWN_FEATURE_LEVEL_TM,
+                featureId,
+                actionName,
+                outputData,
+                JsonPointer.of("features/" + featureId + "/inbox/messages/" + actionName),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateFeatureEventData(final String featureId, final String eventName,
+            @Nullable final JsonValue data,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateFeatureEventData(KNOWN_FEATURE_LEVEL_TM,
+                featureId,
+                eventName,
+                data,
+                JsonPointer.of("features/" + featureId + "/outbox/messages/" + eventName),
+                provideValidationContext()
+        ));
+    }
+
+    private static ValidationContext provideValidationContext() {
+        return ValidationContext.buildValidationContext(DittoHeaders.empty(), null, null);
+    }
+
+    private static void internalCheckFail(final boolean mustFail, final CompletionStage<Void> stage) {
+        if (mustFail) {
+            assertThat(stage)
+                    .isCompletedExceptionally()
+                    .failsWithin(50, TimeUnit.MILLISECONDS)
+                    .withThrowableOfType(ExecutionException.class)
+                    .withCauseInstanceOf(WotThingModelPayloadValidationException.class);
+        } else {
+            assertThat(stage).isNotCompletedExceptionally().isDone();
+        }
+    }
+}
\ No newline at end of file
diff --git a/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java
new file mode 100644
index 0000000000..b9ad26d1dc
--- /dev/null
+++ b/wot/validation/src/test/java/org/eclipse/ditto/wot/validation/WotThingModelValidationThingLevelTest.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (c) 2024 Contributors to the Eclipse Foundation
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.ditto.wot.validation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+import org.eclipse.ditto.base.model.headers.DittoHeaders;
+import org.eclipse.ditto.json.JsonArray;
+import org.eclipse.ditto.json.JsonObject;
+import org.eclipse.ditto.json.JsonPointer;
+import org.eclipse.ditto.json.JsonValue;
+import org.eclipse.ditto.things.model.Attributes;
+import org.eclipse.ditto.wot.model.Action;
+import org.eclipse.ditto.wot.model.Actions;
+import org.eclipse.ditto.wot.model.AtContext;
+import org.eclipse.ditto.wot.model.Event;
+import org.eclipse.ditto.wot.model.Events;
+import org.eclipse.ditto.wot.model.Properties;
+import org.eclipse.ditto.wot.model.Property;
+import org.eclipse.ditto.wot.model.SingleDataSchema;
+import org.eclipse.ditto.wot.model.ThingModel;
+import org.eclipse.ditto.wot.model.TmOptional;
+import org.eclipse.ditto.wot.model.TmOptionalElement;
+import org.eclipse.ditto.wot.validation.config.ThingValidationConfig;
+import org.eclipse.ditto.wot.validation.config.TmValidationConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Provides unit tests for testing the "thing" related functionality of {@link WotThingModelValidation}.
+ */
+public final class WotThingModelValidationThingLevelTest {
+
+    private static final String PROP_SOME_BOOL = "someBool";
+    private static final String PROP_SOME_INT = "someInt";
+    private static final String PROP_SOME_NUMBER = "someNumber";
+    private static final String PROP_SOME_STRING = "someString";
+    private static final String PROP_SOME_ARRAY_STRINGS = "someArray_strings";
+    private static final String PROP_SOME_OBJECT = "someObject";
+
+    private static final JsonObject PROP_KNOWN_SOME_OBJECT = JsonObject.newBuilder()
+            .set(PROP_SOME_BOOL, false)
+            .set(PROP_SOME_INT, 3)
+            .set(PROP_SOME_STRING, "helo")
+            .build();
+
+    private static final Properties KNOWN_PROPERTIES = Properties.from(List.of(
+            Property.newBuilder(PROP_SOME_BOOL)
+                    .setSchema(SingleDataSchema.newBooleanSchemaBuilder().build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_INT)
+                    .setSchema(SingleDataSchema.newIntegerSchemaBuilder().build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_NUMBER)
+                    .setSchema(SingleDataSchema.newNumberSchemaBuilder().build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_STRING)
+                    .setSchema(SingleDataSchema.newStringSchemaBuilder().build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_ARRAY_STRINGS)
+                    .setSchema(SingleDataSchema.newArraySchemaBuilder()
+                            .setItems(SingleDataSchema.newStringSchemaBuilder().build())
+                            .build())
+                    .build(),
+            Property.newBuilder(PROP_SOME_OBJECT)
+                    .setSchema(SingleDataSchema.newObjectSchemaBuilder()
+                            .setProperties(Map.of(
+                                    PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                    PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                    PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                            ))
+                            .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                            .enhanceObjectBuilder(builder -> builder.set("additionalProperties", false))
+                            .build())
+                    .build()
+    ));
+
+    private static final String ACTION_PROCESS_BOOL = "processBool";
+    private static final String ACTION_PROCESS_OBJECT = "processObject";
+
+    private static final Actions KNOWN_ACTIONS = Actions.from(List.of(
+            Action.newBuilder(ACTION_PROCESS_BOOL)
+                    .setInput(SingleDataSchema.newBooleanSchemaBuilder().build())
+                    .setOutput(SingleDataSchema.newBooleanSchemaBuilder().build())
+                    .build(),
+            Action.newBuilder(ACTION_PROCESS_OBJECT)
+                    .setInput(SingleDataSchema.newObjectSchemaBuilder()
+                            .setProperties(Map.of(
+                                    PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                    PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                    PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                            ))
+                            .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                            .build()
+                    )
+                    .setOutput(SingleDataSchema.newObjectSchemaBuilder()
+                            .setProperties(Map.of(
+                                    PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                    PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                    PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                            ))
+                            .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                            .build())
+                    .build()
+    ));
+
+    private static final String EVENT_EMIT_INT = "emitInt";
+    private static final String EVENT_EMIT_ARRAY = "emitArray_objects";
+
+    private static final Events KNOWN_EVENTS = Events.from(List.of(
+            Event.newBuilder(EVENT_EMIT_INT)
+                    .setData(SingleDataSchema.newIntegerSchemaBuilder().build())
+                    .build(),
+            Event.newBuilder(EVENT_EMIT_ARRAY)
+                    .setData(SingleDataSchema.newArraySchemaBuilder()
+                            .setItems(SingleDataSchema.newObjectSchemaBuilder()
+                                    .setProperties(Map.of(
+                                            PROP_SOME_BOOL, SingleDataSchema.newBooleanSchemaBuilder().build(),
+                                            PROP_SOME_INT, SingleDataSchema.newIntegerSchemaBuilder().build(),
+                                            PROP_SOME_STRING, SingleDataSchema.newStringSchemaBuilder().build()
+                                    ))
+                                    .setRequired(List.of(PROP_SOME_BOOL, PROP_SOME_STRING))
+                                    .build())
+                            .build()
+                    )
+                    .build()
+    ));
+
+    private static final ThingModel KNOWN_THING_LEVEL_TM = ThingModel.newBuilder()
+            .setAtContext(AtContext.newSingleUriAtContext("foo"))
+            .setProperties(KNOWN_PROPERTIES)
+            .setTmOptional(TmOptional.of(List.of(
+                    TmOptionalElement.of("/properties/" + PROP_SOME_ARRAY_STRINGS),
+                    TmOptionalElement.of("/properties/" + PROP_SOME_OBJECT)
+            )))
+            .setActions(KNOWN_ACTIONS)
+            .setEvents(KNOWN_EVENTS)
+            .build();
+
+    private static final Attributes KNOWN_THING_ATTRIBUTES = Attributes.newBuilder()
+            .set(PROP_SOME_BOOL, true)
+            .set(PROP_SOME_INT, 42)
+            .set(PROP_SOME_NUMBER, 42.23)
+            .set(PROP_SOME_STRING, "some")
+            .build();
+
+
+    private WotThingModelValidation sut;
+
+    @Before
+    public void setUp() {
+        final TmValidationConfig validationConfig = mock(TmValidationConfig.class);
+        when(validationConfig.isEnabled()).thenReturn(true);
+
+        final ThingValidationConfig thingValidationConfig = mock(ThingValidationConfig.class);
+        when(thingValidationConfig.isEnforceAttributes()).thenReturn(true);
+        when(thingValidationConfig.isForbidNonModeledAttributes()).thenReturn(true);
+        when(thingValidationConfig.isEnforceInboxMessagesInput()).thenReturn(true);
+        when(thingValidationConfig.isEnforceInboxMessagesOutput()).thenReturn(true);
+        when(thingValidationConfig.isEnforceOutboxMessages()).thenReturn(true);
+        when(thingValidationConfig.isForbidNonModeledInboxMessages()).thenReturn(true);
+        when(thingValidationConfig.isForbidNonModeledOutboxMessages()).thenReturn(true);
+        when(validationConfig.getThingValidationConfig()).thenReturn(thingValidationConfig);
+
+        sut = WotThingModelValidation.of(validationConfig);
+    }
+
+    @Test
+    public void validateThingAttributeSucceedsForBooleanAttribute() {
+        checkValidateThingAttribute(PROP_SOME_BOOL, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateThingAttributesSucceedsForBooleanAttribute() {
+        checkValidateThingAttributes(PROP_SOME_BOOL, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForBooleanAttribute() {
+        checkValidateThingAttribute(PROP_SOME_BOOL, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForBooleanAttribute() {
+        checkValidateThingAttributes(PROP_SOME_BOOL, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateThingAttributeSucceedsForIntegerAttribute() {
+        checkValidateThingAttribute(PROP_SOME_INT, JsonValue.of(42), false);
+    }
+
+    @Test
+    public void validateThingAttributesSucceedsForIntegerAttribute() {
+        checkValidateThingAttributes(PROP_SOME_INT, JsonValue.of(42), false);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForIntegerAttribute() {
+        checkValidateThingAttribute(PROP_SOME_INT, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForIntegerAttribute() {
+        checkValidateThingAttributes(PROP_SOME_INT, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateThingAttributeSucceedsForNumberAttribute() {
+        checkValidateThingAttribute(PROP_SOME_NUMBER, JsonValue.of(42.23), false);
+    }
+
+    @Test
+    public void validateThingAttributesSucceedsForNumberAttribute() {
+        checkValidateThingAttributes(PROP_SOME_NUMBER, JsonValue.of(42.23), false);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForNumberAttribute() {
+        checkValidateThingAttribute(PROP_SOME_NUMBER, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForNumberAttribute() {
+        checkValidateThingAttributes(PROP_SOME_NUMBER, JsonValue.of("something else"), true);
+    }
+
+    @Test
+    public void validateThingAttributeSucceedsForStringAttribute() {
+        checkValidateThingAttribute(PROP_SOME_STRING, JsonValue.of("some"), false);
+    }
+
+    @Test
+    public void validateThingAttributesSucceedsForStringAttribute() {
+        checkValidateThingAttributes(PROP_SOME_STRING, JsonValue.of("some"), false);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForStringAttribute() {
+        checkValidateThingAttribute(PROP_SOME_STRING, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForStringAttribute() {
+        checkValidateThingAttributes(PROP_SOME_STRING, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateThingAttributeSucceedsForArraysStringAttribute() {
+        checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, JsonArray.of("some", "string", "arr"), false);
+    }
+
+    @Test
+    public void validateThingAttributesSucceedsForArraysStringAttribute() {
+        checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, JsonArray.of("some", "string", "arr"), false);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForArraysStringAttribute() {
+        checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS, JsonArray.of(false, true, false), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForArraysStringAttribute() {
+        checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS, JsonArray.of(false, true, false), true);
+    }
+
+    @Test
+    public void validateThingAttributeSucceedsForObjectAttribute() {
+        checkValidateThingAttribute(PROP_SOME_OBJECT, PROP_KNOWN_SOME_OBJECT, false);
+    }
+
+    @Test
+    public void validateThingAttributesSucceedsForObjectAttribute() {
+        checkValidateThingAttributes(PROP_SOME_OBJECT, PROP_KNOWN_SOME_OBJECT, false);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForObjectAttribute() {
+        checkValidateThingAttribute(PROP_SOME_OBJECT, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForObjectAttribute() {
+        checkValidateThingAttributes(PROP_SOME_OBJECT, JsonValue.of(false), true);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForObjectAttributeMissingRequiredField() {
+        checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_SOME_STRING).build(), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForObjectAttributeMissingRequiredField() {
+        checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().remove(PROP_SOME_STRING).build(), true);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForObjectAttributeAdditionalNotSpecifiedField() {
+        checkValidateThingAttribute(PROP_SOME_ARRAY_STRINGS,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForObjectAttributeAdditionalNotSpecifiedField() {
+        checkValidateThingAttributes(PROP_SOME_ARRAY_STRINGS,
+                PROP_KNOWN_SOME_OBJECT.toBuilder().set("new", "foo").build(), true);
+    }
+
+    @Test
+    public void validateThingAttributeFailsForNonModeledAttribute() {
+        checkValidateThingAttribute("newStuff", JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateThingAttributesFailsForNonModeledAttribute() {
+        checkValidateThingAttributes("newStuff", JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateThingAttributeDeletionFailsForRequiredBooleanAttribute() {
+        checkValidateThingAttributeDeletion(PROP_SOME_BOOL, true);
+    }
+
+    @Test
+    public void validateThingAttributeDeletionSucceedsForOptionalArrayAttribute() {
+        checkValidateThingAttributeDeletion(PROP_SOME_ARRAY_STRINGS, false);
+    }
+
+    @Test
+    public void validateThingActionBoolInputSucceeds() {
+        checkValidateThingActionInput(ACTION_PROCESS_BOOL, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateThingActionBoolInputFailsWrongDatatype() {
+        checkValidateThingActionInput(ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true);
+    }
+
+    @Test
+    public void validateThingActionObjectInputSucceeds() {
+        checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_SOME_BOOL, false)
+                .set(PROP_SOME_INT, 23)
+                .set(PROP_SOME_STRING, "yes!")
+                .build(), false);
+    }
+
+    @Test
+    public void validateThingActionObjectInputFailsWrongDatatype() {
+        checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonArray.empty(), true);
+    }
+
+    @Test
+    public void validateThingActionObjectInputFailsMissingRequiredField() {
+        checkValidateThingActionInput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_SOME_STRING, "yes!")
+                .build(), true);
+    }
+
+    @Test
+    public void validateNonModeledThingActionInputFails() {
+        checkValidateThingActionInput("someNewAction", JsonValue.of(true), true);
+    }
+
+    @Test
+    public void validateThingActionBoolOutputSucceeds() {
+        checkValidateThingActionOutput(ACTION_PROCESS_BOOL, JsonValue.of(true), false);
+    }
+
+    @Test
+    public void validateThingActionBoolOutputFailsWrongDatatype() {
+        checkValidateThingActionOutput(ACTION_PROCESS_BOOL, JsonValue.of("oh no"), true);
+    }
+
+    @Test
+    public void validateThingActionObjectOutputSucceeds() {
+        checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_SOME_BOOL, false)
+                .set(PROP_SOME_INT, 23)
+                .set(PROP_SOME_STRING, "yes!")
+                .build(), false);
+    }
+
+    @Test
+    public void validateThingActionObjectOutputFailsWrongDatatype() {
+        checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonArray.empty(), true);
+    }
+
+    @Test
+    public void validateThingActionObjectOutputFailsMissingRequiredField() {
+        checkValidateThingActionOutput(ACTION_PROCESS_OBJECT, JsonObject.newBuilder()
+                .set(PROP_SOME_STRING, "yes!")
+                .build(), true);
+    }
+
+    @Test
+    public void validateThingEventIntDataSucceeds() {
+        checkValidateThingEventData(EVENT_EMIT_INT, JsonValue.of(33), false);
+    }
+
+    @Test
+    public void validateThingEventBoolDataFailsWrongDatatype() {
+        checkValidateThingEventData(EVENT_EMIT_INT, JsonValue.of("oh no"), true);
+    }
+
+    @Test
+    public void validateThingEventArrayDataSucceeds() {
+        checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonArray.newBuilder()
+                .add(JsonObject.newBuilder()
+                        .set(PROP_SOME_BOOL, false)
+                        .set(PROP_SOME_INT, 23)
+                        .set(PROP_SOME_STRING, "yes!")
+                        .build()
+                ).build(), false);
+    }
+
+    @Test
+    public void validateThingEventArrayDataFailsWrongDatatype() {
+        checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonObject.empty(), true);
+    }
+
+    @Test
+    public void validateThingEventArrayDataFailsMissingRequiredFieldInsideObject() {
+        checkValidateThingEventData(EVENT_EMIT_ARRAY, JsonArray.newBuilder()
+                .add(JsonObject.newBuilder()
+                        .set(PROP_SOME_INT, 23)
+                        .set(PROP_SOME_STRING, "yes!")
+                        .build()
+                ).build(), true);
+    }
+
+    @Test
+    public void validateNonModeledThingEventDataFails() {
+        checkValidateThingEventData("someNewEvent", JsonValue.of(true), true);
+    }
+
+    private void checkValidateThingAttribute(final CharSequence attributePath, final JsonValue attributeValue,
+            final boolean mustFail
+    ) {
+        final JsonPointer attributePointer = JsonPointer.of(attributePath);
+        internalCheckFail(mustFail, sut.validateThingAttribute(KNOWN_THING_LEVEL_TM,
+                attributePointer,
+                attributeValue,
+                JsonPointer.of("attributes").append(attributePointer),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateThingAttributes(final CharSequence attributePath, final JsonValue attributeValue,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateThingAttributes(KNOWN_THING_LEVEL_TM,
+                KNOWN_THING_ATTRIBUTES.toBuilder()
+                        .set(attributePath, attributeValue)
+                        .build(),
+                JsonPointer.of("attributes"),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateThingAttributeDeletion(final CharSequence attributePath, final boolean mustFail) {
+        internalCheckFail(mustFail, sut.validateThingScopedDeletion(KNOWN_THING_LEVEL_TM,
+                Map.of(),
+                JsonPointer.of("attributes/" + attributePath),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateThingActionInput(final String actionName, @Nullable final JsonValue inputData,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateThingActionInput(KNOWN_THING_LEVEL_TM,
+                actionName,
+                inputData,
+                JsonPointer.of("inbox/messages/" + actionName),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateThingActionOutput(final String actionName, @Nullable final JsonValue outputData,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateThingActionOutput(KNOWN_THING_LEVEL_TM,
+                actionName,
+                outputData,
+                JsonPointer.of("inbox/messages/" + actionName),
+                provideValidationContext()
+        ));
+    }
+
+    private void checkValidateThingEventData(final String eventName, @Nullable final JsonValue data,
+            final boolean mustFail
+    ) {
+        internalCheckFail(mustFail, sut.validateThingEventData(KNOWN_THING_LEVEL_TM,
+                eventName,
+                data,
+                JsonPointer.of("outbox/messages/" + eventName),
+                provideValidationContext()
+        ));
+    }
+
+    private static ValidationContext provideValidationContext() {
+        return ValidationContext.buildValidationContext(DittoHeaders.empty(), null, null);
+    }
+
+    private static void internalCheckFail(final boolean mustFail, final CompletionStage<Void> stage) {
+        if (mustFail) {
+            assertThat(stage)
+                    .isCompletedExceptionally()
+                    .failsWithin(50, TimeUnit.MILLISECONDS)
+                    .withThrowableOfType(ExecutionException.class)
+                    .withCauseInstanceOf(WotThingModelPayloadValidationException.class);
+        } else {
+            assertThat(stage).isNotCompletedExceptionally().isDone();
+        }
+    }
+}
\ No newline at end of file