diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag
index d4e166a34a2b62150d76dfa0decf3c0a827bb649..4baff8e5763af0b3a1c8870fa1d2b0a935bb9a24 100644
--- a/ragconnect.base/src/main/jastadd/Analysis.jrag
+++ b/ragconnect.base/src/main/jastadd/Analysis.jrag
@@ -15,6 +15,19 @@ aspect Analysis {
     }
     return numberOfSameDefs > 1;
   }
+  eq RelationEndpointTarget.isAlreadyDefined() {
+    // define lookup here, as not used elsewhere
+    int numberOfSameDefs = 0;
+    for (EndpointTarget target : ragconnect().givenEndpointTargetList()) {
+      if (target.isRelationEndpointTarget()) {
+        RelationEndpointTarget other = target.asRelationEndpointTarget();
+        if (other.getRole().equals(this.getRole())) {
+          numberOfSameDefs += 1;
+        }
+      }
+    }
+    return numberOfSameDefs > 1;
+  }
   eq TokenEndpointTarget.isAlreadyDefined() {
     return lookupTokenEndpointDefinitions(getToken()).stream()
         .filter(containingEndpointDefinition()::matchesType)
@@ -49,7 +62,8 @@ aspect Analysis {
     return target.primitivePrettyPrint().equals(this.primitivePrettyPrint());
   }
   syn String JavaTypeUse.primitivePrettyPrint() {
-    switch (getName()) {
+    String name = getName();
+    switch (name) {
       case "boolean":
       case "Boolean":
         return "boolean";
@@ -78,6 +92,7 @@ aspect Analysis {
 
   syn boolean EndpointTarget.hasAttributeResetMethod();
   eq AttributeEndpointTarget.hasAttributeResetMethod() = false;
+  eq RelationEndpointTarget.hasAttributeResetMethod() = false;
   eq TokenEndpointTarget.hasAttributeResetMethod() = getToken().getNTA();
   eq TypeEndpointTarget.hasAttributeResetMethod() = getType().getNTA();
   eq ContextFreeTypeEndpointTarget.hasAttributeResetMethod() = false;
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd
index 5ca17cdc4ca04db353dbf073fc0cff3d8dd7df93..69ff5d8932cd33a97e44b2fa3562b13cd52617f2 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.jadd
+++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd
@@ -59,6 +59,7 @@ aspect SharedMustache {
           case "warn":
           case "error":
           case "exception":
+            //noinspection DuplicateBranchesInSwitch
             return "ASTNode." + logConsoleErr();
           default:
             return "unknownLoggingLevelForConsole_" + level + "_";
@@ -74,9 +75,8 @@ aspect SharedMustache {
     }
   }
   syn boolean EndpointTarget.typeIsList() = false;
-  eq TypeEndpointTarget.typeIsList() {
-    return getType().isListComponent();
-  }
+  eq TypeEndpointTarget.typeIsList() = getType().isListComponent();
+  eq RelationEndpointTarget.typeIsList() = getRole().isListRole();
 
   syn boolean EndpointTarget.typeIsOpt() = false;
   eq TypeEndpointTarget.typeIsOpt() {
@@ -215,6 +215,10 @@ aspect MustacheMappingApplicationAndDefinition {
   eq MAttributeSendDefinition.preemptiveExpectedValue() = lastValueGetterCall();
   eq MAttributeSendDefinition.preemptiveReturn() = "return false;";
 
+  eq MRelationSendDefinition.firstInputVarName() = getterMethodCall();
+  eq MRelationSendDefinition.preemptiveExpectedValue() = lastValueGetterCall();
+  eq MRelationSendDefinition.preemptiveReturn() = "return false;";
+
   eq MTokenReceiveDefinition.firstInputVarName() = "message";
   eq MTokenReceiveDefinition.preemptiveExpectedValue() = getterMethodCall();
   eq MTokenReceiveDefinition.preemptiveReturn() = "return;";
@@ -425,6 +429,15 @@ aspect MustacheReceiveAndSendAndHandleUri {
   eq AttributeEndpointTarget.parentTypeName() = getParentTypeDecl().getName();
   eq AttributeEndpointTarget.entityName() = getName();
 
+  eq RelationEndpointTarget.getterMethodName() = forwardingNTA_Name();
+  eq RelationEndpointTarget.parentTypeName() = getRole().getType().getName();
+  eq RelationEndpointTarget.entityName() = getRole().getName();
+  eq RelationEndpointTarget.realGetterMethodName() = "get" + getRole().getterMethodName();
+  eq RelationEndpointTarget.realGetterMethodCall() = realGetterMethodName() + (containingEndpointDefinition().indexedSend() ? "(index)" : "()");
+
+  syn String NavigableRole.getterMethodName() = getName();
+  eq ListRole.getterMethodName() = getName() + "List";
+
   eq TokenEndpointTarget.getterMethodName() = "get" + getToken().getName();
   eq TokenEndpointTarget.parentTypeName() = getToken().containingTypeDecl().getName();
   eq TokenEndpointTarget.entityName() = getToken().getName();
@@ -481,6 +494,8 @@ aspect MustacheSendDefinition {
   syn String EndpointDefinition.forwardingNTA_Name() = getEndpointTarget().forwardingNTA_Name();
   syn String EndpointDefinition.forwardingNTA_Type() = getEndpointTarget().forwardingNTA_Type();
 
+  syn boolean EndpointDefinition.relationEndpointWithListRole() = getEndpointTarget().relationEndpointWithListRole();
+
   syn String EndpointDefinition.senderName() = getEndpointTarget().senderName();
 
   syn boolean EndpointDefinition.shouldNotResetValue() = getSend() && !getEndpointTarget().hasAttributeResetMethod();
@@ -494,13 +509,17 @@ aspect MustacheSendDefinition {
   // === attributes needed for computing above ones ===
   syn boolean EndpointTarget.needForwardingNTA() = false;
   eq TypeEndpointTarget.needForwardingNTA() = containingEndpointDefinition().getSend() && !getType().getNTA();
+  eq RelationEndpointTarget.needForwardingNTA() = containingEndpointDefinition().getSend();
 
-  syn String EndpointTarget.forwardingNTA_Name() = null;
+  syn String EndpointTarget.forwardingNTA_Name() = null;  // only needed, if needForwardingNTA evaluates to true
   eq TypeEndpointTarget.forwardingNTA_Name() = ragconnect().internalRagConnectPrefix() + getType().getName();
+  eq RelationEndpointTarget.forwardingNTA_Name() = ragconnect().internalRagConnectPrefix() + getRole().getName();
 
-  syn String EndpointTarget.forwardingNTA_Type() = null;
+  syn String EndpointTarget.forwardingNTA_Type() = null;  // only needed, if needForwardingNTA evaluates to true
   eq TypeEndpointTarget.forwardingNTA_Type() = getType().forwardingNTA_Type(
           containingEndpointDefinition().getIndexBasedListAccess());
+  eq RelationEndpointTarget.forwardingNTA_Type() = getRole().forwardingNTA_Type(
+containingEndpointDefinition().getIndexBasedListAccess());
 
   syn String TypeComponent.forwardingNTA_Type(boolean indexBasedListAccess);
   eq NormalComponent.forwardingNTA_Type(boolean indexBasedListAccess) = getTypeDecl().getName();
@@ -510,6 +529,14 @@ aspect MustacheSendDefinition {
           getTypeDecl().getName() :
           ragconnect().configJastAddList() + "<" + getTypeDecl().getName() + ">";
 
+  syn String Role.forwardingNTA_Type(boolean indexBasedListAccess) = oppositeRole().getType().getName();
+  eq ListRole.forwardingNTA_Type(boolean indexBasedListAccess) = indexBasedListAccess ?
+          oppositeRole().getType().getName() :
+          "java.util.List<" + oppositeRole().getType().getName() + ">";
+
+  syn boolean EndpointTarget.relationEndpointWithListRole() = false;
+  eq RelationEndpointTarget.relationEndpointWithListRole() = getRole().isListRole();
+
   syn String EndpointTarget.senderName() = ragconnect().internalRagConnectPrefix() + "_sender_" + entityName();
   eq ContextFreeTypeEndpointTarget.senderName() = null;
 
@@ -519,6 +546,9 @@ aspect MustacheSendDefinition {
   eq MAttributeSendDefinition.updateMethodName() = ragconnect().internalRagConnectPrefix() + "_update_attr_" + getEndpointDefinition().entityName();
   eq MAttributeSendDefinition.writeMethodName() = ragconnect().internalRagConnectPrefix() + "_writeLastValue_attr_" + getEndpointDefinition().entityName();
 
+  eq MRelationSendDefinition.updateMethodName() = ragconnect().internalRagConnectPrefix() + "_update_" + getEndpointDefinition().entityName();
+  eq MRelationSendDefinition.writeMethodName() = ragconnect().internalRagConnectPrefix() + "_writeLastValue_" + getEndpointDefinition().entityName();
+
   eq MTokenReceiveDefinition.updateMethodName() = null;
   eq MTokenReceiveDefinition.writeMethodName() = null;
 
@@ -641,6 +671,12 @@ aspect AttributesForMustache {
     }
     return new MAttributeSendDefinition();
   }
+  MEndpointDefinition RelationEndpointTarget.createMEndpointDefinition(boolean isSend) {
+    if (!isSend) {
+      throw new IllegalArgumentException("RelationEndpointTarget can only be sent!");
+    }
+    return new MRelationSendDefinition();
+  }
   MEndpointDefinition TokenEndpointTarget.createMEndpointDefinition(boolean isSend) {
     return isSend ? new MTokenSendDefinition() : new MTokenReceiveDefinition();
   }
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.relast b/ragconnect.base/src/main/jastadd/Intermediate.relast
index 486158a4a4094291a756cf8c7715ed6104f5e252..8fa32f92b7bc1fdc0b6fad196b82523a660dc6c2 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.relast
+++ b/ragconnect.base/src/main/jastadd/Intermediate.relast
@@ -2,6 +2,7 @@ abstract MEndpointDefinition ::= InnerMappingDefinition:MInnerMappingDefinition*
 rel MEndpointDefinition.EndpointDefinition -> EndpointDefinition;
 
 MAttributeSendDefinition : MEndpointDefinition;
+MRelationSendDefinition : MEndpointDefinition;
 abstract MTokenEndpointDefinition : MEndpointDefinition;
 MTokenReceiveDefinition : MTokenEndpointDefinition;
 MTokenSendDefinition : MTokenEndpointDefinition;
diff --git a/ragconnect.base/src/main/jastadd/Mappings.jrag b/ragconnect.base/src/main/jastadd/Mappings.jrag
index aa070695a83e31f819d62308b094679b27125a8e..9fd55aa4cad7eb22f75913f7942b547ca91ddec2 100644
--- a/ragconnect.base/src/main/jastadd/Mappings.jrag
+++ b/ragconnect.base/src/main/jastadd/Mappings.jrag
@@ -1,10 +1,18 @@
 aspect DefaultMappings {
 
   private String RagConnect.baseDefaultMappingTypeNamePart(String typeName) {
-    return capitalize(typeName).replace("[]", "s").replace("<", "").replace(">", "List");
+    if (typeName.contains(".")) {
+      StringBuilder sb = new StringBuilder();
+      for (String part : typeName.split("\\.")) {
+        sb.append(baseDefaultMappingTypeNamePart(part));
+      }
+      return sb.toString();
+    } else {
+      return capitalize(typeName).replace("[]", "s").replace("<", "").replace(">", "List");
+    }
   }
 
-  private MappingDefinitionType RagConnect.baseDefaultMappingTypeFromName(String typeName) {
+  private MappingDefinitionType RagConnect.baseDefaultMappingTypeName(String typeName) {
     return typeName.endsWith("[]") ?
         new JavaArrayMappingDefinitionType(new SimpleJavaTypeUse(typeName.replace("[]", ""))) :
         new JavaMappingDefinitionType(new SimpleJavaTypeUse(typeName));
@@ -13,9 +21,9 @@ aspect DefaultMappings {
   private DefaultMappingDefinition RagConnect.createDefaultMappingDefinition(String prefix, String fromTypeName, String toTypeName, String content) {
     DefaultMappingDefinition result = new DefaultMappingDefinition();
     result.setID(prefix + baseDefaultMappingTypeNamePart(fromTypeName) + "To" + baseDefaultMappingTypeNamePart(toTypeName) + "Mapping");
-    result.setFromType(baseDefaultMappingTypeFromName(fromTypeName));
+    result.setFromType(baseDefaultMappingTypeName(fromTypeName));
     result.setFromVariableName("input");
-    result.setToType(baseDefaultMappingTypeFromName(toTypeName));
+    result.setToType(baseDefaultMappingTypeName(toTypeName));
     result.setContent(content);
     return result;
   }
@@ -67,7 +75,7 @@ aspect DefaultMappings {
     );
   }
 
-  syn nta DefaultMappingDefinition RagConnect.defaultBytesToListTreeMapping(String typeName) {
+  syn nta DefaultMappingDefinition RagConnect.defaultBytesToListMapping(String typeName) {
     return treeDefaultMappingDefinition("byte[]", configJastAddList() + "<" + typeName + ">",
         "String content = new String(input);\n" +
             "com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();\n" +
@@ -78,7 +86,7 @@ aspect DefaultMappings {
             "return result;"
     );
   }
-  syn nta DefaultMappingDefinition RagConnect.defaultListTreeToBytesMapping() {
+  syn nta DefaultMappingDefinition RagConnect.defaultListToBytesMapping() {
     return treeDefaultMappingDefinition(configJastAddList(), "byte[]",
         "java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();\n" +
             "com.fasterxml.jackson.core.JsonFactory factory = new com.fasterxml.jackson.core.JsonFactory();\n" +
@@ -88,6 +96,16 @@ aspect DefaultMappings {
             "return outputStream.toString().getBytes();"
     );
   }
+  syn nta DefaultMappingDefinition RagConnect.defaultJavaUtilListToBytesMapping() {
+    return treeDefaultMappingDefinition("java.util.List", "byte[]",
+        "java.io.ByteArrayOutputStream outputStream = new java.io.ByteArrayOutputStream();\n" +
+            "com.fasterxml.jackson.core.JsonFactory factory = new com.fasterxml.jackson.core.JsonFactory();\n" +
+            "com.fasterxml.jackson.core.JsonGenerator generator = factory.createGenerator(outputStream, com.fasterxml.jackson.core.JsonEncoding.UTF8);\n" +
+            "serializeJavaUtilList(input, generator);\n" +
+            "generator.flush();\n" +
+            "return outputStream.toString().getBytes();"
+    );
+  }
 
   syn nta DefaultMappingDefinition RagConnect.defaultBooleanToBytesMapping() = baseDefaultMappingDefinition(
       "boolean", "byte[]", "return java.nio.ByteBuffer.allocate(1).put((byte) (input ? 1 : 0)).array();");
@@ -165,6 +183,10 @@ aspect Mappings {
   }
 
   // --- isPrimitiveType ---
+  syn boolean EndpointDefinition.isPrimitiveType() = getEndpointTarget().isPrimitiveType();
+  syn boolean EndpointTarget.isPrimitiveType() = false;
+  eq TokenEndpointTarget.isPrimitiveType() = getToken().isPrimitiveType();
+  eq AttributeEndpointTarget.isPrimitiveType() = new SimpleJavaTypeUse(getTypeName()).isPrimitiveType();
   syn boolean TokenComponent.isPrimitiveType() = effectiveJavaTypeUse().isPrimitiveType();
   syn boolean JavaTypeUse.isPrimitiveType() = false;
   eq SimpleJavaTypeUse.isPrimitiveType() {
@@ -218,7 +240,7 @@ aspect Mappings {
       default:
         try {
           TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName());
-          return getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListTreeMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName());
+          return getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess() ? ragconnect().defaultBytesToListMapping(typeDecl.getName()) : ragconnect().defaultBytesToTreeMapping(typeDecl.getName());
         } catch (Exception ignore) {
         }
         System.err.println("Could not find suitable default mapping for " + targetTypeName() + " on " + this);
@@ -254,7 +276,10 @@ aspect Mappings {
         return ragconnect().defaultStringToBytesMapping();
       default:
         if (getEndpointTarget().isTypeEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) {
-          return ragconnect().defaultListTreeToBytesMapping();
+          return ragconnect().defaultListToBytesMapping();
+        }
+        if (getEndpointTarget().isRelationEndpointTarget() && typeIsList() && !getIndexBasedListAccess()) {
+          return ragconnect().defaultJavaUtilListToBytesMapping();
         }
         try {
           TypeDecl typeDecl = program().resolveTypeDecl(targetTypeName());
@@ -281,10 +306,14 @@ aspect Mappings {
   }
   syn String EndpointTarget.targetTypeName();
   eq AttributeEndpointTarget.targetTypeName() = getTypeName();
+  eq RelationEndpointTarget.targetTypeName() = getRole().oppositeRole().targetTypeName();
   eq TokenEndpointTarget.targetTypeName() = getToken().effectiveJavaTypeUse().getName();
   eq TypeEndpointTarget.targetTypeName() = getType().getTypeDecl().getName();
   eq ContextFreeTypeEndpointTarget.targetTypeName() = getTypeDecl().getName();
 
+  syn String Role.targetTypeName() = getType().getName();
+  eq ListRole.targetTypeName() = "java.util.List<" + getType().getName() + ">";
+
   //  eq ReceiveFromRestDefinition.suitableDefaultMapping() {
   //    String typeName = getMappingList().isEmpty() ?
   //        getToken().getJavaTypeUse().getName() :
@@ -360,9 +389,10 @@ aspect Mappings {
     for (TypeDecl typeDecl : getProgram().typeDecls()) {
       result.add(defaultBytesToTreeMapping(typeDecl.getName()));
       result.add(defaultTreeToBytesMapping(typeDecl.getName()));
-      result.add(defaultBytesToListTreeMapping(typeDecl.getName()));
+      result.add(defaultBytesToListMapping(typeDecl.getName()));
     }
-    result.add(defaultListTreeToBytesMapping());
+    result.add(defaultListToBytesMapping());
+    result.add(defaultJavaUtilListToBytesMapping());
 //    // string conversion
 //    result.add(defaultStringToBooleanMapping());
 //    result.add(defaultStringToIntMapping());
diff --git a/ragconnect.base/src/main/jastadd/NameResolution.jrag b/ragconnect.base/src/main/jastadd/NameResolution.jrag
index 6d9b1a78abb66f3628d66c259d92f1e90dce94b4..0690eca4a260b5f7396dfe2ca6905b3aa98e0aa5 100644
--- a/ragconnect.base/src/main/jastadd/NameResolution.jrag
+++ b/ragconnect.base/src/main/jastadd/NameResolution.jrag
@@ -146,4 +146,39 @@ aspect RagConnectNameResolution {
     return null;
   }
 
+  // rel ___ -> Navigable
+  refine RefResolverStubs eq ASTNode.globallyResolveNavigableRoleByToken(String id) {
+    NavigableRole result = tryGloballyResolveNavigableRoleByToken(id);
+    if (result == null) {
+      System.err.println("Could not resolve role '" + id + "'.");
+    }
+    return result;
+  }
+  syn NavigableRole ASTNode.tryGloballyResolveNavigableRoleByToken(String id) {
+    // id is of the form 'type_name + "." + role_name'
+    int dotIndex = id.indexOf(".");
+    String typeName = id.substring(0, dotIndex);
+    String roleName = id.substring(dotIndex + 1);
+    for (Relation relation : program().relations()) {
+      if (relation.isDirectedRelation()) {
+        if (relation.asDirectedRelation().getSource().matches(typeName, roleName)) {
+          return relation.asDirectedRelation().getSource();
+        }
+      } else {
+        if (relation.asBidirectionalRelation().getLeft().matches(typeName, roleName)) {
+          return relation.asBidirectionalRelation().getLeft();
+        }
+        if (relation.asBidirectionalRelation().getRight().matches(typeName, roleName)) {
+          return relation.asBidirectionalRelation().getRight();
+        }
+      }
+    }
+    return null;
+  }
+
+  syn boolean Role.matches(String typeName, String roleName) = false;
+  eq NavigableRole.matches(String typeName, String roleName) {
+    return getType().getName().equals(typeName) && getName().equals(roleName);
+  }
+
 }
diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag
index dac193142b1382f01b61e696a0f9d5b02dd8b038..5288a9f14f2cc8e3298d3d20fa53076fd72be09e 100644
--- a/ragconnect.base/src/main/jastadd/Navigation.jrag
+++ b/ragconnect.base/src/main/jastadd/Navigation.jrag
@@ -1,4 +1,4 @@
-aspect NewStuff {
+aspect GeneratedNavigation {
 
   /** Tests if EndpointTarget is a TokenEndpointTarget.
   *  @return 'true' if this is a TokenEndpointTarget, otherwise 'false'
@@ -30,6 +30,12 @@ aspect NewStuff {
   syn boolean EndpointTarget.isAttributeEndpointTarget() = false;
   eq AttributeEndpointTarget.isAttributeEndpointTarget() = true;
 
+  /** Tests if EndpointTarget is a RelationEndpointTarget.
+  *  @return 'true' if this is a RelationEndpointTarget, otherwise 'false'
+  */
+  syn boolean EndpointTarget.isRelationEndpointTarget() = false;
+  eq RelationEndpointTarget.isRelationEndpointTarget() = true;
+
   /** casts a EndpointTarget into a TokenEndpointTarget if possible.
    *  @return 'this' cast to a TokenEndpointTarget or 'null'
    */
@@ -64,6 +70,13 @@ aspect NewStuff {
   syn AttributeEndpointTarget EndpointTarget.asAttributeEndpointTarget();
   eq EndpointTarget.asAttributeEndpointTarget() = null;
   eq AttributeEndpointTarget.asAttributeEndpointTarget() = this;
+
+  /** casts a EndpointTarget into a RelationEndpointTarget if possible.
+   *  @return 'this' cast to a RelationEndpointTarget or 'null'
+   */
+  syn RelationEndpointTarget EndpointTarget.asRelationEndpointTarget();
+  eq EndpointTarget.asRelationEndpointTarget() = null;
+  eq RelationEndpointTarget.asRelationEndpointTarget() = this;
 }
 aspect RagConnectNavigation {
 
@@ -121,6 +134,13 @@ aspect RagConnectNavigation {
   // --- effectiveJavaTypeUse (should be in preprocessor) ---
   syn lazy JavaTypeUse TokenComponent.effectiveJavaTypeUse() = hasJavaTypeUse() ? getJavaTypeUse() : new SimpleJavaTypeUse("String");
 
+  // --- oppositeRole ---
+  inh Role Role.oppositeRole();
+  eq DirectedRelation.getSource().oppositeRole() = getTarget();
+  eq DirectedRelation.getTarget().oppositeRole() = getSource();
+  eq BidirectionalRelation.getLeft().oppositeRole() = getRight();
+  eq BidirectionalRelation.getRight().oppositeRole() = getLeft();
+
   // --- isDefaultMappingDefinition ---
   syn boolean MappingDefinition.isDefaultMappingDefinition() = false;
   eq DefaultMappingDefinition.isDefaultMappingDefinition() = true;
diff --git a/ragconnect.base/src/main/jastadd/RagConnect.relast b/ragconnect.base/src/main/jastadd/RagConnect.relast
index 5605324e99fd6206f4dc4c7eb3a92da3342e224d..5037cff1ffa0a7c803ce0d74207637417b1afbda 100644
--- a/ragconnect.base/src/main/jastadd/RagConnect.relast
+++ b/ragconnect.base/src/main/jastadd/RagConnect.relast
@@ -13,12 +13,11 @@ TypeEndpointTarget : EndpointTarget;
 rel TypeEndpointTarget.Type <-> TypeComponent.TypeEndpointTarget*;
 ContextFreeTypeEndpointTarget : EndpointTarget;
 rel ContextFreeTypeEndpointTarget.TypeDecl <-> TypeDecl.ContextFreeTypeEndpointTarget*;
-UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName> <IsAttribute:boolean>;  // only used by parser
-// to be integrated:
 AttributeEndpointTarget : EndpointTarget ::= <Name> <TypeName> ;
 rel AttributeEndpointTarget.ParentTypeDecl <-> TypeDecl.AttributeEndpointTarget*;
-//RelationEndpointTarget : EndpointTarget ;
-//rel RelationEndpointTarget.Role <-> Role.RelationEndpointTarget* ;
+RelationEndpointTarget : EndpointTarget ;
+rel RelationEndpointTarget.Role <-> NavigableRole.RelationEndpointTarget* ;
+UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName> <IsAttribute:boolean>;  // only used by parser
 
 DependencyDefinition ::= <ID>;
 rel DependencyDefinition.Source <-> TokenComponent.DependencySourceDefinition*;
diff --git a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
index 04e8cadc459653830e5f29e2d189b1aa2c6c0ad5..70f778c7b8f63b86d2d60bf0d71d1beb6cdeb41e 100644
--- a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
+++ b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
@@ -16,6 +16,14 @@ aspect ParserRewrites {
       return result;
     }
 
+    when (getChildName() != null && tryGloballyResolveNavigableRoleByToken(combinedName()) != null)
+    to RelationEndpointTarget {
+      RelationEndpointTarget result = new RelationEndpointTarget();
+      result.copyOtherValuesFrom(this);
+      result.setRole(NavigableRole.createRef(this.combinedName()));
+      return result;
+    }
+
     when (getChildName() == "")
     to ContextFreeTypeEndpointTarget {
       ContextFreeTypeEndpointTarget result = new ContextFreeTypeEndpointTarget();
diff --git a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
index dfe3f9789681b3dee17c7fec7b2c671ef400808c..f671349843dddf5e81099660365668fbf5ae4c66 100644
--- a/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
+++ b/ragconnect.base/src/main/java/org/jastadd/ragconnect/compiler/Compiler.java
@@ -124,9 +124,7 @@ public class Compiler extends AbstractCompiler {
       compiler.run(args);
     } catch (CompilerException e) {
       System.err.println(e.getMessage());
-      if (compiler.isVerbose()) {
-        e.printStackTrace();
-      }
+      e.printStackTrace();
       System.exit(1);
     }
   }
diff --git a/ragconnect.base/src/main/resources/ListAspect.mustache b/ragconnect.base/src/main/resources/ListAspect.mustache
index 31eaf5ac82a5854956077403e92124c9b6160353..e7ec88e5e579e7a7b50bbeb83d0779bc64203991 100644
--- a/ragconnect.base/src/main/resources/ListAspect.mustache
+++ b/ragconnect.base/src/main/resources/ListAspect.mustache
@@ -11,6 +11,19 @@ public void {{configJastAddList}}.serialize(com.fasterxml.jackson.core.JsonGener
   }
 }
 
+protected static <T extends ASTNode> void ASTNode.serializeJavaUtilList(
+        java.util.List<T> input, com.fasterxml.jackson.core.JsonGenerator g) throws SerializationException {
+  try {
+    g.writeStartArray();
+    for (T child : input) {
+      child.serialize(g);
+    }
+    g.writeEndArray();
+  } catch (java.io.IOException e) {
+    throw new SerializationException("unable to serialize list", e);
+  }
+}
+
 {{#typesForReceivingListEndpoints}}
 public static {{configJastAddList}}<{{Name}}> {{Name}}.deserializeList(com.fasterxml.jackson.databind.node.ArrayNode node) throws DeserializationException {
   {{configJastAddList}}<{{Name}}> result = new {{configJastAddList}}<>();
diff --git a/ragconnect.base/src/main/resources/mappingApplication.mustache b/ragconnect.base/src/main/resources/mappingApplication.mustache
index 836e8d438397db9fc8e862c828e5b57eeb34d2f1..482d54783782ada82cacd9ce75b81d30c1e21e68 100644
--- a/ragconnect.base/src/main/resources/mappingApplication.mustache
+++ b/ragconnect.base/src/main/resources/mappingApplication.mustache
@@ -1,3 +1,10 @@
+{{#Send}}
+{{^PrimitiveType}}
+if ({{firstInputVarName}} == null) {
+  {{preemptiveReturn}}
+}
+{{/PrimitiveType}}
+{{/Send}}
 {{{lastDefinitionToType}}} {{lastResult}};
 try {
   {{#innerMappingDefinitions}}
diff --git a/ragconnect.base/src/main/resources/sendDefinition.mustache b/ragconnect.base/src/main/resources/sendDefinition.mustache
index b066c690444a48cb8aadd64ccdd2dcdc2a46f78d..1e66fdedcc7620aefe31a89fcece47dbcf9708ac 100644
--- a/ragconnect.base/src/main/resources/sendDefinition.mustache
+++ b/ragconnect.base/src/main/resources/sendDefinition.mustache
@@ -14,7 +14,13 @@ public boolean {{parentTypeName}}.{{connectMethodName}}(String {{connectParamete
         {{#configLoggingEnabledForWrites}}
         {{logDebug}}("[Send] {{entityName}} = {{log_}} -> {{log_}}", {{getterMethodCall}}, {{connectParameterName}});
         {{/configLoggingEnabledForWrites}}
-        handler.publish(topic, {{lastValueGetterCall}});
+        if ({{lastValueGetterCall}} != null) {
+          handler.publish(topic, {{lastValueGetterCall}});
+        {{#configLoggingEnabledForWrites}}
+        } else {
+          {{logWarn}}("[Send] {{entityName}} -> {{log_}}: can't send null.", {{connectParameterName}});
+        {{/configLoggingEnabledForWrites}}
+        }
         }{{#IndexBasedListAccess}}, index{{/IndexBasedListAccess}}, connectToken);
       {{updateMethodName}}({{#IndexBasedListAccess}}index{{/IndexBasedListAccess}});
       if (writeCurrentValue) {
@@ -117,5 +123,20 @@ protected void {{parentTypeName}}.{{writeMethodName}}({{#IndexBasedListAccess}}i
 }
 
 {{#needForwardingNTA}}
-syn {{{forwardingNTA_Type}}} {{parentTypeName}}.{{forwardingNTA_Name}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) = {{realGetterMethodCall}}.{{touchedTerminalsMethodName}}();
+syn {{{forwardingNTA_Type}}} {{parentTypeName}}.{{forwardingNTA_Name}}({{#IndexBasedListAccess}}int index{{/IndexBasedListAccess}}) {
+{{#relationEndpointWithListRole}}
+//  for (var element : {{realGetterMethodCall}}) {
+//    element.{{touchedTerminalsMethodName}}();
+//  }
+  {{realGetterMethodCall}}.stream().forEach(element -> element.{{touchedTerminalsMethodName}}());
+  return {{realGetterMethodCall}};
+{{/relationEndpointWithListRole}}
+{{^relationEndpointWithListRole}}
+  {{{forwardingNTA_Type}}} result = {{realGetterMethodCall}};
+  if (result == null) {
+    return null;
+  }
+  return result.{{touchedTerminalsMethodName}}();
+{{/relationEndpointWithListRole}}
+}
 {{/needForwardingNTA}}
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index 468c22acdfb055d1820c30158049248e4e0b98be..821617bef166830e8c3710b81034a6e962073ee1 100644
--- a/ragconnect.tests/build.gradle
+++ b/ragconnect.tests/build.gradle
@@ -151,6 +151,8 @@ def JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL = JASTADD_INCREMENTAL_OPTIONS.clone
 JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL.set(0, '--tracing=all')
 JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL.set(1, '--incremental=param,debug')
 
+classes.dependsOn(':ragconnect.base:jar')
+
 // --- Test: Example ---
 task compileExampleTest(type: RagConnectTest) {
     ragconnect {
@@ -657,7 +659,30 @@ task compileAttributeIncremental(type: RagConnectTest) {
         extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL
     }
 }
-compileAttributeIncremental.outputs.upToDateWhen { false }
+
+// --- Test: relation-incremental ---
+task compileRelationIncremental(type: RagConnectTest) {
+    ragconnect {
+        outputDir = file('src/test/02-after-ragconnect/relationInc')
+        inputFiles = [file('src/test/01-input/relation/Test.relast'),
+                      file('src/test/01-input/relation/Test.connect')]
+        rootNode = 'Root'
+        logWrites = true
+        extraOptions = defaultRagConnectOptionsAnd(['--experimental-jastadd-329'])
+    }
+    relast {
+        useJastAddNames = true
+        grammarName = 'src/test/03-after-relast/relationInc/relationInc'
+        serializer = 'jackson'
+    }
+    jastadd {
+        jastAddList = 'JastAddList'
+        packageName = 'relationInc.ast'
+        inputFiles = [file('src/test/01-input/relation/Test.jadd')]
+        extraOptions = JASTADD_INCREMENTAL_OPTIONS_TRACING_FULL
+    }
+}
+compileRelationIncremental.outputs.upToDateWhen { false }
 
 static ArrayList<String> defaultRagConnectOptionsAnd(ArrayList<String> options = []) {
     if (!options.contains('--logTarget=slf4j')) {
diff --git a/ragconnect.tests/src/test/01-input/relation/README.md b/ragconnect.tests/src/test/01-input/relation/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..12556e1ad76171b40a2e4a8e58717dfa41d4c52b
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/relation/README.md
@@ -0,0 +1,3 @@
+# Relation
+
+Idea: Use send definitions for relations.
diff --git a/ragconnect.tests/src/test/01-input/relation/Test.connect b/ragconnect.tests/src/test/01-input/relation/Test.connect
new file mode 100644
index 0000000000000000000000000000000000000000..61e81d25e3a3bcb898a59112340402c7f21ecccc
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/relation/Test.connect
@@ -0,0 +1,43 @@
+send SenderRoot.MyA;
+send SenderRoot.OptionalA;
+send SenderRoot.ManyA;
+
+send SenderRoot.BiMyA;
+send SenderRoot.BiOptionalA;
+send SenderRoot.BiManyA;
+
+send SenderRoot.MyB using ConcatValues;
+send SenderRoot.OptionalB using ConcatValues;
+send SenderRoot.ManyB using ConcatValueList;
+
+send SenderRoot.BiMyB using ConcatValues;
+send SenderRoot.BiOptionalB using ConcatValues;
+send SenderRoot.BiManyB using ConcatValueList;
+
+ConcatValues maps B b to String {:
+  return b.getValue() + "+" + b.getInner().getInnerValue();
+:}
+
+ConcatValueList maps java.util.List<B> list to String {:
+  StringBuilder sb = new StringBuilder();
+  for (B b : list) {
+    sb.append(b.getValue() + "+" + b.getInner().getInnerValue()).append(";");
+  }
+  return sb.toString();
+:}
+
+receive ReceiverRoot.FromMyA;
+receive ReceiverRoot.FromOptionalA;
+receive ReceiverRoot.FromManyA;
+
+receive ReceiverRoot.FromBiMyA;
+receive ReceiverRoot.FromBiOptionalA;
+receive ReceiverRoot.FromBiManyA;
+
+receive ReceiverRoot.FromMyB;
+receive ReceiverRoot.FromOptionalB;
+receive ReceiverRoot.FromManyB;
+
+receive ReceiverRoot.FromBiMyB;
+receive ReceiverRoot.FromBiOptionalB;
+receive ReceiverRoot.FromBiManyB;
diff --git a/ragconnect.tests/src/test/01-input/relation/Test.jadd b/ragconnect.tests/src/test/01-input/relation/Test.jadd
new file mode 100644
index 0000000000000000000000000000000000000000..ee797705faeb4814f881f9778a539c508402d77b
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/relation/Test.jadd
@@ -0,0 +1,36 @@
+aspect Computation {
+}
+aspect MakeCodeCompile {
+
+}
+aspect MakeCodeWork {
+}
+aspect NameResolution {
+  // overriding customID guarantees to produce the same JSON representation for equal lists
+  // otherwise, the value for id is different each time
+  @Override
+  protected String A.customID() {
+    return getClass().getSimpleName() + getValue();
+  }
+  @Override
+  protected String B.customID() {
+    return getClass().getSimpleName() + getValue();
+  }
+  @Override
+  protected String Inner.customID() {
+    return getClass().getSimpleName() + getInnerValue();
+  }
+
+  // override resolving of SenderRoot for As in ReceiverRoot
+  refine RefResolverStubs eq ASTNode.globallyResolveSenderRootByToken(String id) = getParent() != null && !getParent().isListWithoutParent() ? resolveSenderRootInh(id) : null;
+
+  syn boolean ASTNode.isList() = false;
+  eq JastAddList.isList() = true;
+  syn boolean ASTNode.isListWithoutParent() = isList() && getParent() == null;
+
+  inh SenderRoot ASTNode.resolveSenderRootInh(String id);
+  eq SenderRoot.getChild().resolveSenderRootInh(String id) = globallyResolveSenderRootByToken(id);
+  eq ReceiverRoot.getChild().resolveSenderRootInh(String id) = getFakeSenderRoot(id);
+
+  syn nta SenderRoot ReceiverRoot.getFakeSenderRoot(String id) = new SenderRoot();
+}
diff --git a/ragconnect.tests/src/test/01-input/relation/Test.relast b/ragconnect.tests/src/test/01-input/relation/Test.relast
new file mode 100644
index 0000000000000000000000000000000000000000..7effc867e0f5f45f444ac87607be607f1a23e31a
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/relation/Test.relast
@@ -0,0 +1,28 @@
+Root ::= SenderRoot* ReceiverRoot;
+SenderRoot ::= A* B* ;
+rel SenderRoot.MyA -> A;
+rel SenderRoot.OptionalA? -> A;
+rel SenderRoot.ManyA* -> A;
+
+rel SenderRoot.BiMyA <-> A.ToMyA?;
+rel SenderRoot.BiOptionalA? <-> A.ToOptionalA?;
+rel SenderRoot.BiManyA* <-> A.ToManyA?;
+
+rel SenderRoot.MyB -> B;
+rel SenderRoot.OptionalB? -> B;
+rel SenderRoot.ManyB* -> B;
+
+rel SenderRoot.BiMyB <-> B.ToMyB?;
+rel SenderRoot.BiOptionalB? <-> B.ToOptionalB?;
+rel SenderRoot.BiManyB* <-> B.ToManyB?;
+
+ReceiverRoot ::=
+FromMyA:A   FromOptionalA:A   FromManyA:A*
+FromBiMyA:A FromBiOptionalA:A FromBiManyA:A*
+<FromMyB:String>   <FromOptionalB:String>   <FromManyB:String>
+<FromBiMyB:String> <FromBiOptionalB:String> <FromBiManyB:String>
+;
+
+A ::= <Value> Inner ;
+B ::= <Value> Inner ;
+Inner ::= <InnerValue> ;
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
index a3394ea07518bd5ca668fee774c7702ef5334c35..655d61e1710014d37c7aeaa3fdcc140995568f9d 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/TestUtils.java
@@ -3,6 +3,7 @@ package org.jastadd.ragconnect.tests;
 import com.fasterxml.jackson.core.JsonEncoding;
 import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.core.JsonGenerator;
+import org.assertj.core.groups.Tuple;
 import org.awaitility.Awaitility;
 import org.awaitility.core.ConditionFactory;
 import org.jastadd.ragconnect.compiler.Compiler;
@@ -20,6 +21,8 @@ import java.nio.file.Paths;
 import java.util.*;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -155,6 +158,134 @@ public class TestUtils {
             event, node, attribute, params, value);
   }
 
+  public static class TestChecker {
+    static class ActualAndExpected<T> {
+      Callable<T> actual;
+      T expected;
+      BiConsumer<String, T> customCheck;
+
+      ActualAndExpected(String key) {
+        expected = null;
+      }
+
+      void check(String name) {
+        if (customCheck != null) {
+          customCheck.accept(name, expected);
+          return;
+        }
+        if (actual == null) {
+          fail("No actual getter defined for " + name);
+        }
+        T actualValue = null;
+        try {
+          actualValue = this.actual.call();
+        } catch (Exception e) {
+          fail(e);
+        }
+        assertThat(actualValue).as(name).isEqualTo(expected);
+      }
+
+      void checkAwait(String name) {
+        if (customCheck != null) {
+          logger.warn("Custom check set for {}. Can't await for that.", name);
+          customCheck.accept(name, expected);
+          return;
+        }
+        if (actual == null) {
+          fail("No actual getter defined for " + name);
+        }
+        awaitMqtt().alias(name).until(actual, Predicate.isEqual(expected));
+      }
+    }
+    private final static String NUMBER_OF_VALUES = "numberOfValues";
+    private final Map<String, ActualAndExpected<String>> stringValues = new HashMap<>();
+    private final Map<String, ActualAndExpected<Tuple>> tupleValues = new HashMap<>();
+    private final Map<String, ActualAndExpected<Integer>> intValues = new HashMap<>();
+    private boolean needManualWait = true;
+
+    public TestChecker incNumberOfValues() {
+      return addToNumberOfValues(1);
+    }
+
+    public TestChecker addToNumberOfValues(int increment) {
+      // if there is at least one call to this, we do not need to manually wait in the next check()
+      needManualWait = false;
+      Integer currentExpected = intValues.computeIfAbsent(NUMBER_OF_VALUES, ActualAndExpected::new).expected;
+      return put(NUMBER_OF_VALUES, currentExpected + increment);
+    }
+
+    public TestChecker setActualNumberOfValues(Callable<Integer> actual) {
+      setActualInteger(NUMBER_OF_VALUES, actual);
+      intValues.get(NUMBER_OF_VALUES).expected = 0;
+      return this;
+    }
+
+    public TestChecker setActualString(String name, Callable<String> actual) {
+      stringValues.computeIfAbsent(name, ActualAndExpected::new).actual = actual;
+      return this;
+    }
+
+    public TestChecker setCheckForString(String name, BiConsumer<String, String> check) {
+      stringValues.computeIfAbsent(name, ActualAndExpected::new).customCheck = check;
+      return this;
+    }
+
+    public TestChecker put(String name, String expected) {
+      stringValues.computeIfAbsent(name, ActualAndExpected::new).expected = expected;
+      return this;
+    }
+
+    public TestChecker setActualTuple(String name, Callable<Tuple> actual) {
+      tupleValues.computeIfAbsent(name, ActualAndExpected::new).actual = actual;
+      return this;
+    }
+
+    public TestChecker setCheckForTuple(String name, BiConsumer<String, Tuple> check) {
+      tupleValues.computeIfAbsent(name, ActualAndExpected::new).customCheck = check;
+      return this;
+    }
+
+    public TestChecker put(String name, Tuple expected) {
+      tupleValues.computeIfAbsent(name, ActualAndExpected::new).expected = expected;
+      return this;
+    }
+
+    public TestChecker setActualInteger(String name, Callable<Integer> actual) {
+      intValues.computeIfAbsent(name, ActualAndExpected::new).actual = actual;
+      return this;
+    }
+
+    public TestChecker setCheckForInteger(String name, BiConsumer<String, Integer> check) {
+      intValues.computeIfAbsent(name, ActualAndExpected::new).customCheck = check;
+      return this;
+    }
+
+    public TestChecker put(String name, Integer expected) {
+      intValues.computeIfAbsent(name, ActualAndExpected::new).expected = expected;
+      return this;
+    }
+
+    public void check() {
+      if (needManualWait) {
+        try {
+          waitForMqtt();
+        } catch (InterruptedException e) {
+          fail(e);
+        }
+      }
+      intValues.get(NUMBER_OF_VALUES).checkAwait(NUMBER_OF_VALUES);
+
+      stringValues.forEach((name, aae) -> aae.check(name));
+      tupleValues.forEach((name, aae) -> aae.check(name));
+      intValues.forEach((name, aae) -> {
+        if (!name.equals(NUMBER_OF_VALUES)) {
+          aae.check(name);
+        }
+      });
+      needManualWait = true;
+    }
+  }
+
   public static class IntList {
     private final List<Integer> integers = newArrayList();
     public IntList(Integer... values) {
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/relation/RelationTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/relation/RelationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a88f97fefabd47159f91d268a96ffe67a3dcee7e
--- /dev/null
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/relation/RelationTest.java
@@ -0,0 +1,690 @@
+package org.jastadd.ragconnect.tests.relation;
+
+import org.jastadd.ragconnect.tests.AbstractMqttTest;
+import org.jastadd.ragconnect.tests.TestUtils;
+import org.junit.jupiter.api.Tag;
+import relationInc.ast.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.jastadd.ragconnect.tests.TestUtils.TestChecker;
+import static org.jastadd.ragconnect.tests.TestUtils.mqttUri;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Test case "relation".
+ *
+ * @author rschoene - Initial contribution
+ */
+@Tag("Incremental")
+@Tag("New")
+public class RelationTest extends AbstractMqttTest {
+
+  private static final String TOPIC_WILDCARD = "rel/#";
+  private static final String TOPIC_MY_A = "rel/my_a";
+  private static final String TOPIC_OPTIONAL_A = "rel/optional_a";
+  private static final String TOPIC_MANY_A = "rel/many_a";
+  private static final String TOPIC_BI_MY_A = "rel/bi_my_a";
+  private static final String TOPIC_BI_OPTIONAL_A = "rel/bi_optional_a";
+  private static final String TOPIC_BI_MANY_A = "rel/bi_many_a";
+  private static final String TOPIC_MY_B = "rel/my_b";
+  private static final String TOPIC_OPTIONAL_B = "rel/optional_b";
+  private static final String TOPIC_MANY_B = "rel/many_b";
+  private static final String TOPIC_BI_MY_B = "rel/bi_my_b";
+  private static final String TOPIC_BI_OPTIONAL_B = "rel/bi_optional_b";
+  private static final String TOPIC_BI_MANY_B = "rel/bi_many_b";
+
+  private MqttHandler handler;
+  private ReceiverData data;
+  private TestChecker checker;
+
+  private Root model;
+  private SenderRoot senderUni;
+  private SenderRoot senderBi;
+  private ReceiverRoot receiverRoot;
+
+  @Override
+  protected void createModel() {
+    model = new Root();
+//    model.trace().setReceiver(TestUtils::logEvent);
+    senderUni = createSenderRoot("uni");
+    senderUni.setMyA(uniA(1));
+    senderUni.setMyB(uniB(1));
+
+    senderBi = createSenderRoot("bi");
+    senderBi.setBiMyA(biA(1));
+    senderBi.setBiMyB(biB(1));
+
+    receiverRoot = new ReceiverRoot();
+    model.addSenderRoot(senderUni);
+    model.addSenderRoot(senderBi);
+    model.setReceiverRoot(receiverRoot);
+  }
+
+  private SenderRoot createSenderRoot(String name) {
+    SenderRoot result = new SenderRoot();
+    A dummyA = createA(name + "-dummyA");
+    result.addA(dummyA);
+    result.addA(createA(name + "-a1"));
+    result.addA(createA(name + "-a2"));
+    result.addA(createA(name + "-a3"));
+
+    B dummyB = createB(name + "-dummyB");
+    result.addB(dummyB);
+    result.addB(createB(name + "-b1"));
+    result.addB(createB(name + "-b2"));
+    result.addB(createB(name + "-b3"));
+
+    result.setMyA(dummyA);
+    result.setBiMyA(dummyA);
+    result.setMyB(dummyB);
+    result.setBiMyB(dummyB);
+
+    return result;
+  }
+
+  private A createA(String value) {
+    return new A().setValue(value)
+            .setInner(new Inner().setInnerValue("inner-" + value));
+  }
+
+  private B createB(String value) {
+    return new B().setValue(value)
+            .setInner(new Inner().setInnerValue("inner-" + value));
+  }
+
+  private A uniA(int index) {
+    return senderUni.getA(index);
+  }
+
+  private B uniB(int index) {
+    return senderUni.getB(index);
+  }
+
+  private A biA(int index) {
+    return senderBi.getA(index);
+  }
+
+  private B biB(int index) {
+    return senderBi.getB(index);
+  }
+
+  @Override
+  protected void setupReceiverAndConnect() throws IOException, InterruptedException {
+    model.ragconnectSetupMqttWaitUntilReady(2, TimeUnit.SECONDS);
+    handler = new MqttHandler().setHost(TestUtils.getMqttHost()).dontSendWelcomeMessage();
+    assertTrue(handler.waitUntilReady(2, TimeUnit.SECONDS));
+
+    data = new ReceiverData();
+    assertTrue(handler.newConnection(TOPIC_WILDCARD, bytes -> data.numberOfValues += 1));
+
+    checker = new TestChecker();
+    checker.setActualNumberOfValues(() -> data.numberOfValues).setCheckForString(TOPIC_MY_A,
+                    (name, expected) -> assertNullOrA(expected, receiverRoot.getFromMyA(), name))
+            .setCheckForString(TOPIC_OPTIONAL_A,
+                    (name, expected) -> assertNullOrA(expected, receiverRoot.getFromOptionalA(), name))
+            .setCheckForTuple(TOPIC_MANY_A,
+                    (name, expected) -> assertListEqualsForA(expected.toList(), receiverRoot.getFromManyAList(), name))
+            .setCheckForString(TOPIC_BI_MY_A,
+                    (name, expected) -> assertNullOrA(expected, receiverRoot.getFromBiMyA(), name))
+            .setCheckForString(TOPIC_BI_OPTIONAL_A,
+                    (name, expected) -> assertNullOrA(expected, receiverRoot.getFromBiOptionalA(), name))
+            .setCheckForTuple(TOPIC_BI_MANY_A,
+                    (name, expected) -> assertListEqualsForA(expected.toList(), receiverRoot.getFromBiManyAList(), name))
+            .setCheckForString(TOPIC_MY_B,
+                    (name, expected) -> assertNullOrB(expected, receiverRoot.getFromMyB(), name))
+            .setCheckForString(TOPIC_OPTIONAL_B,
+                    (name, expected) -> assertNullOrB(expected, receiverRoot.getFromOptionalB(), name))
+            .setCheckForTuple(TOPIC_MANY_B,
+                    (name, expected) -> assertListEqualsForB(expected.toList(), receiverRoot.getFromManyB(), name))
+            .setCheckForString(TOPIC_BI_MY_B,
+                    (name, expected) -> assertNullOrB(expected, receiverRoot.getFromBiMyB(), name))
+            .setCheckForString(TOPIC_BI_OPTIONAL_B,
+                    (name, expected) -> assertNullOrB(expected, receiverRoot.getFromBiOptionalB(), name))
+            .setCheckForTuple(TOPIC_BI_MANY_B,
+                    (name, expected) -> assertListEqualsForB(expected.toList(), receiverRoot.getFromBiManyB(), name));
+
+    // connect receive
+    assertTrue(receiverRoot.connectFromMyA(mqttUri(TOPIC_MY_A)));
+    assertTrue(receiverRoot.connectFromOptionalA(mqttUri(TOPIC_OPTIONAL_A)));
+    assertTrue(receiverRoot.connectFromManyAList(mqttUri(TOPIC_MANY_A)));
+    assertTrue(receiverRoot.connectFromBiMyA(mqttUri(TOPIC_BI_MY_A)));
+    assertTrue(receiverRoot.connectFromBiOptionalA(mqttUri(TOPIC_BI_OPTIONAL_A)));
+    assertTrue(receiverRoot.connectFromBiManyAList(mqttUri(TOPIC_BI_MANY_A)));
+    assertTrue(receiverRoot.connectFromMyB(mqttUri(TOPIC_MY_B)));
+    assertTrue(receiverRoot.connectFromOptionalB(mqttUri(TOPIC_OPTIONAL_B)));
+    assertTrue(receiverRoot.connectFromManyB(mqttUri(TOPIC_MANY_B)));
+    assertTrue(receiverRoot.connectFromBiMyB(mqttUri(TOPIC_BI_MY_B)));
+    assertTrue(receiverRoot.connectFromBiOptionalB(mqttUri(TOPIC_BI_OPTIONAL_B)));
+    assertTrue(receiverRoot.connectFromBiManyB(mqttUri(TOPIC_BI_MANY_B)));
+
+    // connect send, and wait to receive (if writeCurrentValue is set)
+    assertTrue(senderUni.connectMyA(mqttUri(TOPIC_MY_A), isWriteCurrentValue()));
+    assertTrue(senderUni.connectOptionalA(mqttUri(TOPIC_OPTIONAL_A), isWriteCurrentValue()));
+    assertTrue(senderUni.connectManyA(mqttUri(TOPIC_MANY_A), isWriteCurrentValue()));
+
+    assertTrue(senderBi.connectBiMyA(mqttUri(TOPIC_BI_MY_A), isWriteCurrentValue()));
+    assertTrue(senderBi.connectBiOptionalA(mqttUri(TOPIC_BI_OPTIONAL_A), isWriteCurrentValue()));
+    assertTrue(senderBi.connectBiManyA(mqttUri(TOPIC_BI_MANY_A), isWriteCurrentValue()));
+
+    assertTrue(senderUni.connectMyB(mqttUri(TOPIC_MY_B), isWriteCurrentValue()));
+    assertTrue(senderUni.connectOptionalB(mqttUri(TOPIC_OPTIONAL_B), isWriteCurrentValue()));
+    assertTrue(senderUni.connectManyB(mqttUri(TOPIC_MANY_B), isWriteCurrentValue()));
+
+    assertTrue(senderBi.connectBiMyB(mqttUri(TOPIC_BI_MY_B), isWriteCurrentValue()));
+    assertTrue(senderBi.connectBiOptionalB(mqttUri(TOPIC_BI_OPTIONAL_B), isWriteCurrentValue()));
+    assertTrue(senderBi.connectBiManyB(mqttUri(TOPIC_BI_MANY_B), isWriteCurrentValue()));
+  }
+
+  @Override
+  protected void communicateSendInitialValue() throws IOException {
+    checker.addToNumberOfValues(8)
+            .put(TOPIC_MY_A, "uni-a1")
+            .put(TOPIC_OPTIONAL_A, (String) null)
+            .put(TOPIC_MANY_A, tuple())
+            .put(TOPIC_BI_MY_A, "bi-a1")
+            .put(TOPIC_BI_OPTIONAL_A, (String) null)
+            .put(TOPIC_BI_MANY_A, tuple())
+            .put(TOPIC_MY_B, "uni-b1")
+            .put(TOPIC_OPTIONAL_B, (String) null)
+            .put(TOPIC_MANY_B, tuple())
+            .put(TOPIC_BI_MY_B, "bi-b1")
+            .put(TOPIC_BI_OPTIONAL_B, (String) null)
+            .put(TOPIC_BI_MANY_B, tuple());
+
+    communicateBoth();
+  }
+
+  @Override
+  protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException {
+    checker.put(TOPIC_MY_A, (String) null)
+            .put(TOPIC_OPTIONAL_A, (String) null)
+            .put(TOPIC_MANY_A, tuple())
+            .put(TOPIC_BI_MY_A, (String) null)
+            .put(TOPIC_BI_OPTIONAL_A, (String) null)
+            .put(TOPIC_BI_MANY_A, tuple())
+            .put(TOPIC_MY_B, (String) null)
+            .put(TOPIC_OPTIONAL_B, (String) null)
+            .put(TOPIC_MANY_B, tuple())
+            .put(TOPIC_BI_MY_B, (String) null)
+            .put(TOPIC_BI_OPTIONAL_B, (String) null)
+            .put(TOPIC_BI_MANY_B, tuple());
+
+    communicateBoth();
+  }
+
+  protected void communicateBoth() throws IOException {
+    checker.check();
+
+    // myA -> uni-a1, myB -> uni-b1
+    // --- testing unmapped unidirectional normal role --- //
+
+    uniA(1).setValue("test-1");
+    checker.incNumberOfValues().put(TOPIC_MY_A, "test-1:inner-uni-a1").check();
+
+    senderUni.setMyA(uniA(2));
+    checker.incNumberOfValues().put(TOPIC_MY_A, "uni-a2").check();
+
+    // changing something that was previously the relation target must not trigger a message
+    uniA(1).setValue("test-2-ignored");
+    checker.check();
+
+    uniA(2).setValue("test-3");
+    checker.incNumberOfValues().put(TOPIC_MY_A, "test-3:inner-uni-a2").check();
+
+    uniA(2).getInner().setInnerValue("test-4");
+    checker.incNumberOfValues().put(TOPIC_MY_A, "test-3:test-4").check();
+
+    // setting a new relation target resulting in the same serialization must not trigger a message
+    uniA(1).setValue("test-3");
+    uniA(1).getInner().setInnerValue("test-4");
+    senderUni.setMyA(uniA(1));
+    checker.check();
+
+    // --- testing unmapped unidirectional optional role --- //
+
+    // reset a2
+    uniA(2).setValue("uni-a2");
+    uniA(2).getInner().setInnerValue("inner-uni-a2");
+
+    senderUni.setOptionalA(uniA(2));
+    checker.incNumberOfValues().put(TOPIC_OPTIONAL_A, "uni-a2").check();
+
+    uniA(2).setValue("test-5");
+    checker.incNumberOfValues().put(TOPIC_OPTIONAL_A, "test-5:inner-uni-a2").check();
+
+    senderUni.setOptionalA(uniA(1));
+    checker.incNumberOfValues().put(TOPIC_OPTIONAL_A, "test-3:test-4").check();
+
+    // change a nonterminal target of two relations must trigger two messages
+    uniA(1).getInner().setInnerValue("test-6");
+    checker.addToNumberOfValues(2)
+            .put(TOPIC_MY_A, "test-3:test-6")
+            .put(TOPIC_OPTIONAL_A, "test-3:test-6")
+            .check();
+
+    // setting an optional relation to null is allowed, but must not trigger a message
+    senderUni.setOptionalA(null);
+    checker.check();
+
+    // setting the previous nonterminal as relation target again won't trigger a message
+    senderUni.setOptionalA(uniA(1));
+    checker.check();
+
+    // --- testing unmapped unidirectional list role --- //
+
+    senderUni.addManyA(uniA(3));
+    checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("uni-a3")).check();
+
+    uniA(3).setValue("test-7");
+    checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3")).check();
+
+    senderUni.addManyA(uniA(2));
+    checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-5:inner-uni-a2")).check();
+
+    senderUni.addManyA(uniA(1));
+    checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-5:inner-uni-a2", "test-3:test-6")).check();
+
+    uniA(2).getInner().setInnerValue("test-8");
+    checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-5:test-8", "test-3:test-6")).check();
+
+    senderUni.removeManyA(uniA(2));
+    checker.incNumberOfValues().put(TOPIC_MANY_A, tuple("test-7:inner-uni-a3", "test-3:test-6")).check();
+
+    // disconnect my-a, optional-a, many-a - resetting afterwards must not trigger a message
+    senderUni.disconnectMyA(mqttUri(TOPIC_MY_A));
+    senderUni.disconnectOptionalA(mqttUri(TOPIC_OPTIONAL_A));
+    senderUni.disconnectManyA(mqttUri(TOPIC_MANY_A));
+    uniA(1).setValue("a1");
+    uniA(1).getInner().setInnerValue("inner-a1");
+    uniA(2).setValue("a2");
+    uniA(2).getInner().setInnerValue("inner-a2");
+    uniA(3).setValue("a3");
+    uniA(3).getInner().setInnerValue("inner-a3");
+    checker.check();
+
+    // "reset" values in receiver-root to make check method call shorted
+    receiverRoot.setFromMyA(createA("uni-a1"));
+    receiverRoot.setFromOptionalA(null);
+    receiverRoot.setFromManyAList(new JastAddList<>());
+    checker.put(TOPIC_MY_A, "uni-a1")
+            .put(TOPIC_OPTIONAL_A, (String) null)
+            .put(TOPIC_MANY_A, tuple());
+    checker.check();
+
+    // biMyA -> bi-a1, biMyB -> bi-b1
+    // --- testing unmapped bidirectional normal role --- //
+    biA(1).setValue("test-9");
+    checker.incNumberOfValues().put(TOPIC_BI_MY_A, "test-9:inner-bi-a1").check();
+
+    // set opposite role of relation must trigger message
+    biA(2).setToMyA(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_MY_A, "bi-a2").check();
+
+    // changing something that was previously the relation target must not trigger a message
+    biA(1).setValue("test-9-ignored");
+    checker.check();
+
+    biA(2).setValue("test-10");
+    checker.incNumberOfValues().put(TOPIC_BI_MY_A, "test-10:inner-bi-a2").check();
+
+    biA(2).getInner().setInnerValue("test-11");
+    checker.incNumberOfValues().put(TOPIC_BI_MY_A, "test-10:test-11").check();
+
+    // setting a new relation target resulting in the same serialization must not trigger a message
+    biA(1).setValue("test-10");
+    biA(1).getInner().setInnerValue("test-11");
+    biA(1).setToMyA(senderBi);
+    checker.check();
+
+    // --- testing unmapped bidirectional optional role --- //
+    // reset a2
+    biA(2).setValue("bi-a2");
+    biA(2).getInner().setInnerValue("inner-bi-a2");
+
+    senderBi.setBiOptionalA(biA(2));
+    checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_A, "bi-a2").check();
+
+    biA(2).setValue("test-12");
+    checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_A, "test-12:inner-bi-a2").check();
+
+    // set opposite role of relation must trigger message
+    biA(1).setToOptionalA(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_A, "test-10:test-11").check();
+
+    // change a nonterminal target of two relations must trigger two messages
+    biA(1).getInner().setInnerValue("test-13");
+    checker.addToNumberOfValues(2)
+            .put(TOPIC_BI_MY_A, "test-10:test-13")
+            .put(TOPIC_BI_OPTIONAL_A, "test-10:test-13").check();
+
+    // setting an optional relation to null is allowed, but must not trigger a message
+    senderBi.setBiOptionalA(null);
+    checker.check();
+
+    // setting the previous nonterminal as relation target again won't trigger a message
+    biA(1).setToOptionalA(senderBi);
+    checker.check();
+
+    // --- testing unmapped bidirectional list role --- //
+    biA(3).setToManyA(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("bi-a3")).check();
+
+    biA(3).setValue("test-14");
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3")).check();
+
+    senderBi.addBiManyA(biA(2));
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-12:inner-bi-a2")).check();
+
+    biA(1).setToManyA(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-12:inner-bi-a2", "test-10:test-13")).check();
+
+    biA(2).getInner().setInnerValue("test-15");
+    // currently, an additional message is sent at bi_optional_a for biA1 as the serialization includes relations from A to SenderRoot and the relation to ToManyA was added for biA1.
+    // this appears to be a bug in either jastadd or ragconnect
+    // numberOfValues should actually be only 37 here.
+    checker.addToNumberOfValues(2).put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-12:test-15", "test-10:test-13")).check();
+
+    senderBi.removeBiManyA(biA(2));
+    // the bug from above does not occur here, although the situation is similar
+    // so, only one message is sent
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-14:inner-bi-a3", "test-10:test-13")).check();
+
+    biA(3).setToManyA(null);
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_A, tuple("test-10:test-13")).check();
+
+    // change a nonterminal target of three relations must trigger three messages
+    biA(1).setValue("test-16");
+    checker.addToNumberOfValues(3)
+            .put(TOPIC_BI_MY_A, "test-16:test-13")
+            .put(TOPIC_BI_OPTIONAL_A, "test-16:test-13")
+            .put(TOPIC_BI_MANY_A, tuple("test-16:test-13"))
+            .check();
+
+    // disconnect bi-my-a, bi-optional-a, bi-many-a - resetting afterwards must not trigger a message
+    senderBi.disconnectBiMyA(mqttUri(TOPIC_BI_MY_A));
+    senderBi.disconnectBiOptionalA(mqttUri(TOPIC_BI_OPTIONAL_A));
+    senderBi.disconnectBiManyA(mqttUri(TOPIC_BI_MANY_A));
+    biA(1).setValue("bi-a1");
+    biA(1).getInner().setInnerValue("inner-bi-a1");
+    biA(2).setValue("bi-a2");
+    biA(2).getInner().setInnerValue("inner-bi-a2");
+    biA(3).setValue("bi-a3");
+    biA(3).getInner().setInnerValue("inner-bi-a3");
+    checker.check();
+
+    // "reset" values in receiver-root to make check method call shorted
+    receiverRoot.setFromBiMyA(createA("bi-a1"));
+    receiverRoot.setFromBiOptionalA(null);
+    receiverRoot.setFromBiManyAList(new JastAddList<>());
+    checker.put(TOPIC_BI_MY_A, "bi-a1")
+            .put(TOPIC_BI_OPTIONAL_A, (String) null)
+            .put(TOPIC_BI_MANY_A, tuple())
+            .check();
+
+    // --- testing transformed unidirectional normal role --- //
+    uniB(1).setValue("test-17");
+    checker.incNumberOfValues().put(TOPIC_MY_B, "test-17:inner-uni-b1").check();
+
+    senderUni.setMyB(uniB(2));
+    checker.incNumberOfValues().put(TOPIC_MY_B, "uni-b2").check();
+
+    // changing something that was previously the relation target must not trigger a message
+    uniB(1).setValue("test-17-ignored");
+    checker.check();
+
+    uniB(2).setValue("test-18");
+    checker.incNumberOfValues().put(TOPIC_MY_B, "test-18:inner-uni-b2").check();
+
+    uniB(2).getInner().setInnerValue("test-19");
+    checker.incNumberOfValues().put(TOPIC_MY_B, "test-18:test-19").check();
+
+    // setting a new relation target resulting in the same serialization must not trigger a message
+    uniB(1).setValue("test-18");
+    uniB(1).getInner().setInnerValue("test-19");
+    senderUni.setMyB(uniB(1));
+    checker.check();
+
+    // --- testing transformed unidirectional optional role --- //
+
+    // reset a2
+    uniB(2).setValue("uni-b2");
+    uniB(2).getInner().setInnerValue("inner-uni-b2");
+
+    senderUni.setOptionalB(uniB(2));
+    checker.incNumberOfValues().put(TOPIC_OPTIONAL_B, "uni-b2").check();
+
+    uniB(2).setValue("test-20");
+    checker.incNumberOfValues().put(TOPIC_OPTIONAL_B, "test-20:inner-uni-b2").check();
+
+    senderUni.setOptionalB(uniB(1));
+    checker.incNumberOfValues().put(TOPIC_OPTIONAL_B, "test-18:test-19").check();
+
+    // change a nonterminal target of two relations must trigger two messages
+    uniB(1).getInner().setInnerValue("test-21");
+    checker.addToNumberOfValues(2)
+            .put(TOPIC_MY_B, "test-18:test-21")
+            .put(TOPIC_OPTIONAL_B, "test-18:test-21")
+            .check();
+
+    // setting an optional relation to null is allowed, but must not trigger a message
+    senderUni.setOptionalB(null);
+    checker.check();
+
+    // setting the previous nonterminal as relation target again won't trigger a message
+    senderUni.setOptionalB(uniB(1));
+    checker.check();
+
+    // --- testing transformed unidirectional list role --- //
+    senderUni.addManyB(uniB(3));
+    checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("uni-b3")).check();
+
+    uniB(3).setValue("test-22");
+    checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3")).check();
+
+    senderUni.addManyB(uniB(2));
+    checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-20:inner-uni-b2")).check();
+
+    senderUni.addManyB(uniB(1));
+    checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-20:inner-uni-b2", "test-18:test-21")).check();
+
+    uniB(2).getInner().setInnerValue("test-23");
+    checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-20:test-23", "test-18:test-21")).check();
+
+    senderUni.removeManyB(uniB(2));
+    checker.incNumberOfValues().put(TOPIC_MANY_B, tuple("test-22:inner-uni-b3", "test-18:test-21")).check();
+
+    // disconnect my-b, optional-b, many-b - resetting afterwards must not trigger a message
+    senderUni.disconnectMyB(mqttUri(TOPIC_MY_B));
+    senderUni.disconnectOptionalB(mqttUri(TOPIC_OPTIONAL_B));
+    senderUni.disconnectManyB(mqttUri(TOPIC_MANY_B));
+    uniB(1).setValue("b1");
+    uniB(1).getInner().setInnerValue("inner-b1");
+    uniB(2).setValue("b2");
+    uniB(2).getInner().setInnerValue("inner-b2");
+    uniB(3).setValue("b3");
+    uniB(3).getInner().setInnerValue("inner-b3");
+
+    // "reset" values in receiver-root to make check method call shorted
+    receiverRoot.setFromMyB("uni-b1+inner-uni-b1");
+    receiverRoot.setFromOptionalB(null);
+    receiverRoot.setFromManyB(null);
+    checker.put(TOPIC_MY_B, "uni-b1")
+            .put(TOPIC_OPTIONAL_B, (String) null)
+            .put(TOPIC_MANY_B, tuple())
+            .check();
+
+    // --- testing transformed bidirectional normal role --- //
+    biB(1).setValue("test-24");
+    checker.incNumberOfValues().put(TOPIC_BI_MY_B, "test-24:inner-bi-b1").check();
+
+    // set opposite role of relation must trigger message
+    biB(2).setToMyB(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_MY_B, "bi-b2").check();
+
+    // changing something that was previously the relation target must not trigger a message
+    biB(1).setValue("test-24-ignored");
+    checker.check();
+
+    biB(2).setValue("test-25");
+    checker.incNumberOfValues().put(TOPIC_BI_MY_B, "test-25:inner-bi-b2").check();
+
+    biB(2).getInner().setInnerValue("test-26");
+    checker.incNumberOfValues().put(TOPIC_BI_MY_B, "test-25:test-26").check();
+
+    // setting a new relation target resulting in the same serialization must not trigger a message
+    biB(1).setValue("test-25");
+    biB(1).getInner().setInnerValue("test-26");
+    biB(1).setToMyB(senderBi);
+    checker.check();
+
+    // --- testing transformed bidirectional optional role --- //
+    // reset b2
+    biB(2).setValue("bi-b2");
+    biB(2).getInner().setInnerValue("inner-bi-b2");
+
+    senderBi.setBiOptionalB(biB(2));
+    checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_B, "bi-b2").check();
+
+    biB(2).setValue("test-27");
+    checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_B, "test-27:inner-bi-b2").check();
+
+    // set opposite role of relation must trigger message
+    biB(1).setToOptionalB(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_OPTIONAL_B, "test-25:test-26").check();
+
+    // change a nonterminal target of two relations must trigger two messages
+    biB(1).getInner().setInnerValue("test-28");
+    checker.addToNumberOfValues(2)
+            .put(TOPIC_BI_MY_B, "test-25:test-28")
+            .put(TOPIC_BI_OPTIONAL_B, "test-25:test-28")
+            .check();
+
+    // setting an optional relation to null is allowed, but must not trigger a message
+    senderBi.setBiOptionalB(null);
+    checker.check();
+
+    // setting the previous nonterminal as relation target again won't trigger a message
+    biB(1).setToOptionalB(senderBi);
+    checker.check();
+
+    // --- testing transformed bidirectional list role --- //
+    biB(3).setToManyB(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("bi-b3")).check();
+
+    biB(3).setValue("test-29");
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3")).check();
+
+    senderBi.addBiManyB(biB(2));
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-27:inner-bi-b2")).check();
+
+    biB(1).setToManyB(senderBi);
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-27:inner-bi-b2", "test-25:test-28")).check();
+
+    biB(2).getInner().setInnerValue("test-30");
+    // the bug appearing in the unmapped bidirectional list case does not appear here, because here only a string representation of value + inner value is sent. so changes to relations do not trigger a message
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-27:test-30", "test-25:test-28")).check();
+
+    senderBi.removeBiManyB(biB(2));
+    // the bug from above does not occur here, although the situation is similar
+    // so, only one message is sent
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-29:inner-bi-b3", "test-25:test-28")).check();
+
+    biB(3).setToManyB(null);
+    checker.incNumberOfValues().put(TOPIC_BI_MANY_B, tuple("test-25:test-28")).check();
+
+    // change a nonterminal target of three relations must trigger three messages
+    biB(1).setValue("test-31");
+    checker.addToNumberOfValues(3)
+            .put(TOPIC_BI_MY_B, "test-31:test-28")
+            .put(TOPIC_BI_OPTIONAL_B, "test-31:test-28")
+            .put(TOPIC_BI_MANY_B, tuple("test-31:test-28"))
+            .check();
+
+    // disconnect bi-my-a, bi-optional-a, bi-many-a - resetting afterwards must not trigger a message
+    senderBi.disconnectBiMyB(mqttUri(TOPIC_BI_MY_B));
+    senderBi.disconnectBiOptionalB(mqttUri(TOPIC_BI_OPTIONAL_B));
+    senderBi.disconnectBiManyB(mqttUri(TOPIC_BI_MANY_B));
+    biB(1).setValue("bi-b1");
+    biB(1).getInner().setInnerValue("inner-bi-b1");
+    biB(2).setValue("bi-b2");
+    biB(2).getInner().setInnerValue("inner-bi-b2");
+    biB(3).setValue("bi-b3");
+    biB(3).getInner().setInnerValue("inner-bi-b3");
+    checker.check();
+
+  }
+
+  private void assertNullOrA(String expectedValue, A actual, String alias) {
+    if (expectedValue == null) {
+      assertNull(actual, alias);
+      return;
+    }
+    final String expectedInner;
+    if (expectedValue.contains(":")) {
+      String[] tokens = expectedValue.split(":");
+      assertEquals(2, tokens.length);
+      expectedValue = tokens[0];
+      expectedInner = tokens[1];
+    } else {
+      expectedInner = "inner-" + expectedValue;
+    }
+    assertThat(actual.getValue()).describedAs(alias + ".Value").isEqualTo(expectedValue);
+    assertThat(actual.getInner()).describedAs(alias + ".inner != null").isNotNull();
+    assertThat(actual.getInner().getInnerValue()).describedAs(alias + ".inner.Value").isEqualTo(expectedInner);
+  }
+
+  private void assertListEqualsForA(List<Object> expected, JastAddList<A> actual, String alias) {
+    assertEquals(expected.size(), actual.getNumChild(), alias + ".size");
+    for (int i = 0, expectedSize = expected.size(); i < expectedSize; i++) {
+      String s = (String) expected.get(i);
+      assertNullOrA(s, actual.getChild(i), alias + "[" + i + "]");
+    }
+  }
+
+  private void assertNullOrB(String expectedValue, String actual, String alias) {
+    final String expectedTransformed;
+    if (expectedValue == null) {
+      expectedTransformed = "";
+    } else {
+      final String expectedInner;
+      if (expectedValue.contains(":")) {
+        String[] tokens = expectedValue.split(":");
+        assertEquals(2, tokens.length);
+        expectedValue = tokens[0];
+        expectedInner = tokens[1];
+      } else {
+        expectedInner = "inner-" + expectedValue;
+      }
+      expectedTransformed = expectedValue + "+" + expectedInner;
+    }
+    assertEquals(expectedTransformed, actual, alias);
+  }
+
+  private void assertListEqualsForB(List<Object> expected, String actual, String alias) {
+    String[] actualTokens = actual.isEmpty() ? new String[0] : actual.split(";");
+    assertEquals(expected.size(), actualTokens.length, alias + ".size");
+    for (int i = 0, expectedSize = expected.size(); i < expectedSize; i++) {
+      String s = (String) expected.get(i);
+      assertNullOrB(s, actualTokens[i], alias + "[" + i + "]");
+    }
+  }
+
+  @Override
+  protected void closeConnections() {
+    if (handler != null) {
+      handler.close();
+    }
+    if (model != null) {
+      model.ragconnectCloseConnections();
+    }
+  }
+
+  private static class ReceiverData {
+    int numberOfValues = 0;
+  }
+}