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..c3c513d18e2d8f1de1b29492621c3bffc3796ab2 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,13 @@ aspect MustacheReceiveAndSendAndHandleUri {
   eq AttributeEndpointTarget.parentTypeName() = getParentTypeDecl().getName();
   eq AttributeEndpointTarget.entityName() = getName();
 
+  eq RelationEndpointTarget.getterMethodName() = "get" + getRole().getterMethodName();
+  eq RelationEndpointTarget.parentTypeName() = getRole().getType().getName();
+  eq RelationEndpointTarget.entityName() = getRole().getName();
+
+  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();
@@ -519,6 +530,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 +655,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..b5569995f579ac097cc78bac9a2980775affa39a 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();");
@@ -218,7 +236,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 +272,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 +302,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 +385,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..31cd12365e1158425f45c2666d86faa0bee14c0e 100644
--- a/ragconnect.base/src/main/resources/mappingApplication.mustache
+++ b/ragconnect.base/src/main/resources/mappingApplication.mustache
@@ -1,3 +1,6 @@
+if ({{firstInputVarName}} == null) {
+  {{preemptiveReturn}}
+}
 {{{lastDefinitionToType}}} {{lastResult}};
 try {
   {{#innerMappingDefinitions}}
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index e0250d81e554a31b5aa5abe2f7036200f921d59b..1c80e4eb08857cd472fec4ef64cfd8b029cbfcb0 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 {
diff --git a/ragconnect.tests/src/test/01-input/relation/Test.connect b/ragconnect.tests/src/test/01-input/relation/Test.connect
index f63801b9fb48d7daae3ea7415c2f89449b7d6625..5fdc1f08be19d29600f4c1a314114cfae7cdf99c 100644
--- a/ragconnect.tests/src/test/01-input/relation/Test.connect
+++ b/ragconnect.tests/src/test/01-input/relation/Test.connect
@@ -1,27 +1,27 @@
-//send SenderRoot.MyA;
-//send SenderRoot.OptionalA;
-//send SenderRoot.ListA;
-//
-//send SenderRoot.BiMyA;
-//send SenderRoot.BiOptionalA;
-//send SenderRoot.BiListA;
-//
-//send SenderRoot.MyB using ConcatValues;
-//send SenderRoot.OptionalB using ConcatValues;
-//send SenderRoot.ListB using ConcatValueList;
-//
-//send SenderRoot.BiMyB using ConcatValues;
-//send SenderRoot.BiOptionalB using ConcatValues;
-//send SenderRoot.BiListB using ConcatValueList;
+send SenderRoot.MyA;
+send SenderRoot.OptionalA;
+send SenderRoot.ListA;
+
+send SenderRoot.BiMyA;
+send SenderRoot.BiOptionalA;
+send SenderRoot.BiListA;
+
+send SenderRoot.MyB using ConcatValues;
+send SenderRoot.OptionalB using ConcatValues;
+send SenderRoot.ListB using ConcatValueList;
+
+send SenderRoot.BiMyB using ConcatValues;
+send SenderRoot.BiOptionalB using ConcatValues;
+send SenderRoot.BiListB using ConcatValueList;
 
 ConcatValues maps B b to String {:
-  return b,getValue() + b.getInner().getInnerValue();
+  return b.getValue() + b.getInner().getInnerValue();
 :}
 
-ConcatValueList maps JastAddList<B> list to String {:
+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(";");
+    sb.append(b.getValue() + b.getInner().getInnerValue()).append(";");
   }
   return sb.toString();
 :}
diff --git a/ragconnect.tests/src/test/01-input/relation/Test.jadd b/ragconnect.tests/src/test/01-input/relation/Test.jadd
index 2f54e7f851452f24d623b8d424744ecc8544af79..9ed81e10ad62435704b667aabc5624e2dd959a61 100644
--- a/ragconnect.tests/src/test/01-input/relation/Test.jadd
+++ b/ragconnect.tests/src/test/01-input/relation/Test.jadd
@@ -4,21 +4,18 @@ aspect MakeCodeCompile {
 
 }
 aspect MakeCodeWork {
-  public boolean SenderRoot.connectMyA(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectOptionalA(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectListA(String uriString, boolean sendCurrentValue) { return true; }
-  //
-  public boolean SenderRoot.connectBiMyA(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectBiOptionalA(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectBiListA(String uriString, boolean sendCurrentValue) { return true; }
-  //
-  public boolean SenderRoot.connectMyB(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectOptionalB(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectListB(String uriString, boolean sendCurrentValue) { return true; }
-  //
-  public boolean SenderRoot.connectBiMyB(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectBiOptionalB(String uriString, boolean sendCurrentValue) { return true; }
-  public boolean SenderRoot.connectBiListB(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectMyA(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectOptionalA(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectListA(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectBiMyA(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectBiOptionalA(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectBiListA(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectMyB(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectOptionalB(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectListB(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectBiMyB(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectBiOptionalB(String uriString, boolean sendCurrentValue) { return true; }
+  //public boolean SenderRoot.connectBiListB(String uriString, boolean sendCurrentValue) { return true; }
 }
 aspect NameResolution {
   // overriding customID guarantees to produce the same JSON representation for equal lists
diff --git a/ragconnect.tests/src/test/01-input/relation/Test.relast b/ragconnect.tests/src/test/01-input/relation/Test.relast
index 5c56aefbedb08269b0a57cfd5e3d5fe259fd7760..d0cc7790d80eeeb27de65b68a52cd615bfc5761f 100644
--- a/ragconnect.tests/src/test/01-input/relation/Test.relast
+++ b/ragconnect.tests/src/test/01-input/relation/Test.relast
@@ -4,21 +4,21 @@ rel SenderRoot.MyA -> A;
 rel SenderRoot.OptionalA? -> A;
 rel SenderRoot.ListA* -> A;
 
-rel SenderRoot.BiMyA <-> A.ToMyA;
-rel SenderRoot.BiOptionalA? <-> A.ToOptionalA;
-rel SenderRoot.BiListA* <-> A.ToListA;
+rel SenderRoot.BiMyA <-> A.ToMyA?;
+rel SenderRoot.BiOptionalA? <-> A.ToOptionalA?;
+rel SenderRoot.BiListA* <-> A.ToListA?;
 
 rel SenderRoot.MyB -> B;
 rel SenderRoot.OptionalB? -> B;
 rel SenderRoot.ListB* -> B;
 
-rel SenderRoot.BiMyB <-> B.ToMyB;
-rel SenderRoot.BiOptionalB? <-> B.ToOptionalB;
-rel SenderRoot.BiListB* <-> B.ToListB;
+rel SenderRoot.BiMyB <-> B.ToMyB?;
+rel SenderRoot.BiOptionalB? <-> B.ToOptionalB?;
+rel SenderRoot.BiListB* <-> B.ToListB?;
 
 ReceiverRoot ::=
-FromMyA:A   FromOptionalA:A   FromListA:A
-FromBiMyA:A FromBiOptionalA:A FromBiListA:A
+FromMyA:A   FromOptionalA:A   FromListA:A*
+FromBiMyA:A FromBiOptionalA:A FromBiListA:A*
 <FromMyB:String>   <FromOptionalB:String>   <FromListB:String>
 <FromBiMyB:String> <FromBiOptionalB:String> <FromBiListB:String>
 ;
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RelationTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RelationTest.java
index 1c857f6d8aafabd3304dc40fd7bab6c76878b0cd..bf56da42b08a404357f2bd8ef8951a9b53ae3836 100644
--- a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RelationTest.java
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/RelationTest.java
@@ -1,22 +1,23 @@
 package org.jastadd.ragconnect.tests;
 
 import org.assertj.core.groups.Tuple;
-import org.hamcrest.Matchers;
 import relationInc.ast.*;
 import org.junit.jupiter.api.Tag;
 
 import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 import static java.util.function.Predicate.isEqual;
-import static org.hamcrest.Matchers.equalTo;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.groups.Tuple.tuple;
 import static org.hamcrest.Matchers.hasProperty;
 import static org.jastadd.ragconnect.tests.TestUtils.*;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Test case "relation".
@@ -24,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.fail;
  * @author rschoene - Initial contribution
  */
 @Tag("Incremental")
+@Tag("New")
 public class RelationTest extends AbstractMqttTest {
 
   private static final String TOPIC_WILDCARD = "rel/#";
@@ -96,10 +98,10 @@ public class RelationTest extends AbstractMqttTest {
     // connect receive
     assertTrue(receiverRoot.connectFromMyA(mqttUri(TOPIC_MY_A)));
     assertTrue(receiverRoot.connectFromOptionalA(mqttUri(TOPIC_OPTIONAL_A)));
-    assertTrue(receiverRoot.connectFromListA(mqttUri(TOPIC_LIST_A)));
+    assertTrue(receiverRoot.connectFromListAList(mqttUri(TOPIC_LIST_A)));
     assertTrue(receiverRoot.connectFromBiMyA(mqttUri(TOPIC_BI_MY_A)));
     assertTrue(receiverRoot.connectFromBiOptionalA(mqttUri(TOPIC_BI_OPTIONAL_A)));
-    assertTrue(receiverRoot.connectFromBiListA(mqttUri(TOPIC_BI_LIST_A)));
+    assertTrue(receiverRoot.connectFromBiListAList(mqttUri(TOPIC_BI_LIST_A)));
     assertTrue(receiverRoot.connectFromMyB(mqttUri(TOPIC_MY_B)));
     assertTrue(receiverRoot.connectFromOptionalB(mqttUri(TOPIC_OPTIONAL_B)));
     assertTrue(receiverRoot.connectFromListB(mqttUri(TOPIC_LIST_B)));
@@ -124,10 +126,10 @@ public class RelationTest extends AbstractMqttTest {
     assertTrue(senderBi.connectBiOptionalB(mqttUri(TOPIC_BI_OPTIONAL_B), isWriteCurrentValue()));
     assertTrue(senderBi.connectBiListB(mqttUri(TOPIC_BI_LIST_B), isWriteCurrentValue()));
 
-    waitForNonNull(receiverRoot::getFromMyA);
-    waitForNonNull(receiverRoot::getFromBiMyA);
-    waitForNonNull(receiverRoot::getFromMyB);
-    waitForNonNull(receiverRoot::getFromBiMyB);
+//    waitForNonNull(receiverRoot::getFromMyA);
+//    waitForNonNull(receiverRoot::getFromBiMyA);
+//    waitForNonNull(receiverRoot::getFromMyB);
+//    waitForNonNull(receiverRoot::getFromBiMyB);
   }
 
   private <T> void waitForValue(T expectedValue, Callable<T> callable) {
@@ -146,6 +148,8 @@ public class RelationTest extends AbstractMqttTest {
   protected void communicateSendInitialValue() throws IOException, InterruptedException {
     // TODO implement test
     // TODO also check disconnect
+    check(6, "a1", null, tuple(), null, null, tuple(),
+            "b1", "", tuple(), "", "", tuple());
   }
 
   @Override
@@ -155,52 +159,69 @@ public class RelationTest extends AbstractMqttTest {
     // TODO also check disconnect
   }
 
-  private void checkAs(int numberOfValues, String myA, String optionalA, Tuple listA,
-                       String biMyA, String biOptionalA, Tuple biListA) {
+  private void check(int numberOfValues,
+                     String myA, String optionalA, Tuple listA,
+                     String biMyA, String biOptionalA, Tuple biListA,
+                     String myB, String optionalB, Tuple listB,
+                     String biMyB, String biOptionalB, Tuple biListB) {
     awaitEquals(numberOfValues, () -> data.numberOfValues, "numberOfValues");
 
 //    awaitEquals(Objects.requireNonNullElse(basic, ""),
 //            receiverRoot::getFromBasic, "basic");
 
-    awaitAorNull(myA, receiverRoot::getFromMyA, "myA");
-    awaitAorNull(optionalA, receiverRoot::getFromOptionalA, "myA");
-    // TODO compare list
+    // A values
+    assertNullOrA(myA, receiverRoot.getFromMyA(), "myA");
+    assertNullOrA(optionalA, receiverRoot.getFromOptionalA(), "optionalA");
+    assertListEqualsForA(listA.toList(), receiverRoot.getFromListAList(), "listA");
 
-    awaitAorNull(biMyA, receiverRoot::getFromBiMyA, "biMyA");
-    awaitAorNull(biOptionalA, receiverRoot::getFromBiOptionalA, "biMyA");
-    // TODO compare bi-list
-  }
+    assertNullOrA(biMyA, receiverRoot.getFromBiMyA(), "biMyA");
+    assertNullOrA(biOptionalA, receiverRoot.getFromBiOptionalA(), "biOptionalA");
+    assertListEqualsForA(biListA.toList(), receiverRoot.getFromBiListAList(), "biListA");
 
-  // TODO checkBs
+    // B values
+    assertNullOrB(myB, receiverRoot.getFromMyB(), "myB");
+    assertNullOrB(optionalB, receiverRoot.getFromOptionalB(), "optionalB");
+    assertListEqualsForB(listB.toList(), receiverRoot.getFromListB(), "listB");
 
-  private void awaitNull(Supplier<A> actual, String alias) {
-    awaitMqtt().alias(alias).until(() -> actual.get() == null);
+    assertNullOrB(biMyB, receiverRoot.getFromBiMyB(), "biMyB");
+    assertNullOrB(biOptionalB, receiverRoot.getFromBiOptionalB(), "biOptionalB");
+    assertListEqualsForB(biListB.toList(), receiverRoot.getFromBiListB(), "biListB");
   }
 
   private <T> void awaitEquals(T expected, Callable<T> actual, String alias) {
     awaitMqtt().alias(alias).until(actual, isEqual(expected));
   }
 
-  private void awaitAorNull(String expectedValue, Callable<A> actual, String alias) {
+  private void assertNullOrA(String expectedValue, A actual, String alias) {
     if (expectedValue == null) {
-      awaitNull(convertToSupplier(actual), alias + " null");
+      assertNull(actual, alias);
       return;
     }
     String expectedInner = "inner" + expectedValue;
-    awaitMqtt().alias(alias).until(actual, Matchers.allOf(
-            hasProperty("Value", equalTo(expectedValue)),
-            hasProperty("Inner", hasProperty("InnerValue", equalTo(expectedInner)))));
-  }
-
-  private Supplier<A> convertToSupplier(Callable<A> actual) {
-    return () -> {
-      try {
-        return actual.call();
-      } catch (Exception e) {
-        fail(e);
-        return null;
-      }
-    };
+    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 expected, String actual, String alias) {
+    assertEquals(Objects.requireNonNullElse(expected, ""), actual, alias);
+  }
+
+  private void assertListEqualsForB(List<Object> expected, String actual, String alias) {
+    String[] actualTokens = 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