diff --git a/pages/docs/dsl.md b/pages/docs/dsl.md
index 11bdae78d914918e763cb1a947972036c34ca9b8..3f83c719934a1eebb6a35978fe55c4c01f65c9be 100644
--- a/pages/docs/dsl.md
+++ b/pages/docs/dsl.md
@@ -29,13 +29,53 @@ A breakdown of the parts of that syntax:
   As described above, it can be combined with `indexed`.
   If used on its own, the incoming data is interpreted as a complete list and its elements will be appended to the current list.
 - The `<Non-Terminal>[.<Target>["()"]]` notation describes the actual affected node.
-  - If the target is omitted, all nodes of that non-terminal type can be connected, irrespective of their context.
+  - If the target is omitted, all nodes of that non-terminal type can be connected, irrespective of their context. This is a context-free endpoint definition.
   - The target can be any child on the right-hand side of a production rule, a role of a relation, or an attribute.
     The brackets `()` after the target must be used in case of an attribute, and only then.
 - Optionally, an endpoint can use one or more [mappings](#mappings).
   They will be applied before sending, or after receiving a message.
   Mappings will always be applied in the order they are listed after `using`.
 
+### Context-Free Endpoints
+
+!!! attention
+    Context-Free endpoints are currently only supported for receiving endpoints.
+
+An endpoint with only a non-terminal and without a target is called context-free endpoint.
+Specifying such an endpoint has several consequences:
+
+- The given non-terminal can be connected to in all contexts it occurs as if there were endpoints for all those contexts.
+- There is a special method available on the given non-terminal to connect itself, which selects the correct connect-method depending on its context.
+- Context-sensitive endpoints for this non-terminal can still be specified to modify mappings in this context. If the context is a list, the endpoint must use `indexed` and cannot use `with add`.
+
+**Example**:
+
+```java
+// grammar
+Root ::= A SingleA:A [OptA:A] ListA:A* ;
+A ::= <Value> ;
+
+// connect
+receive A;
+receive Root.SingleA using MyMapping; // specialized endpoint
+```
+
+Implied, additional connect specifications:
+```java
+receive Root.A;
+receive Root.OptA;
+receive indexed Root.ListA;
+```
+
+Application code:
+```java
+A a = root.getOptA();
+// new method on A:
+a.connect("<some-uri-to-connect>");
+// equivalent to (implicitly generated):
+root.connectOptA("<some-uri-to-connect>");
+```
+
 ## Mappings
 
 A mapping is a side effect-free function with one argument (the value that will be transformed) and one result (the transformed value), that will be applied on a value to be sent for a sending endpoint, a received value for a receiving endpoint, or the result of another mapping.
diff --git a/ragconnect.base/src/main/jastadd/Analysis.jrag b/ragconnect.base/src/main/jastadd/Analysis.jrag
index 66e6a5a81aff0823c07e4ec3079551c0740b4963..5659c9321a0016162ee5ca9715d6a38770c2a693 100644
--- a/ragconnect.base/src/main/jastadd/Analysis.jrag
+++ b/ragconnect.base/src/main/jastadd/Analysis.jrag
@@ -12,6 +12,11 @@ aspect Analysis {
         .filter(containingEndpointDefinition()::matchesType)
         .count() > 1;
   }
+  eq ContextFreeTypeEndpointTarget.isAlreadyDefined() {
+    return lookupContextFreeTypeEndpointDefinitions(getTypeDecl()).stream()
+        .filter(containingEndpointDefinition()::matchesType)
+        .count() > 1;
+  }
   syn boolean DependencyDefinition.isAlreadyDefined() = lookupDependencyDefinition(getSource().containingTypeDecl(), getID()) != this;
 
   // --- matchesType ---
@@ -61,6 +66,7 @@ aspect Analysis {
   syn boolean EndpointTarget.entityIsNormalAttribute();
   eq TokenEndpointTarget.entityIsNormalAttribute() = !getToken().getNTA();
   eq TypeEndpointTarget.entityIsNormalAttribute() = !getType().getNTA();
+  eq ContextFreeTypeEndpointTarget.entityIsNormalAttribute() = false;
 
   // --- needProxyToken ---
   syn boolean TokenComponent.needProxyToken() = !getDependencySourceDefinitionList().isEmpty() || getTokenEndpointTargetList().stream().map(EndpointTarget::containingEndpointDefinition).anyMatch(EndpointDefinition::shouldSendValue);
diff --git a/ragconnect.base/src/main/jastadd/Errors.jrag b/ragconnect.base/src/main/jastadd/Errors.jrag
index 61f966507a1d793ad711bb0e3dde085ee0b46ed6..f1924e72f4b5944ee8897390f3db494bfd48c4c9 100644
--- a/ragconnect.base/src/main/jastadd/Errors.jrag
+++ b/ragconnect.base/src/main/jastadd/Errors.jrag
@@ -27,6 +27,16 @@ aspect Errors {
           token().effectiveJavaTypeUse())
     to RagConnect.errors();
 
+  ContextFreeTypeEndpointTarget contributes error("Context-Free endpoint not allowed for root node " +
+      getTypeDecl().getName() + "!")
+    when getTypeDecl().occurencesInProductionRules().isEmpty()
+    to RagConnect.errors();
+
+  EndpointDefinition contributes error("Clash with implied, indexed endpoint definition of context-free endpoint in line " +
+          clashingContextFreeEndpointDefinition().getStartLine() + "!")
+    when !getSend() && clashingContextFreeEndpointDefinition() != null
+    to RagConnect.errors();
+
   DependencyDefinition contributes error("Dependency definition already defined for " + getSource().containingTypeDecl().getName() + " with name " + getID())
     when isAlreadyDefined()
     to RagConnect.errors();
@@ -45,6 +55,18 @@ aspect ErrorHelpers {
     }
     return false;
   }
+
+  syn EndpointDefinition EndpointDefinition.clashingContextFreeEndpointDefinition() {
+    if (getSend() || !typeIsList() || getIndexBasedListAccess()) {
+      return null;
+    }
+    List<EndpointDefinition> contextFreeEndpointsWithSameType = lookupContextFreeTypeEndpointDefinitions(
+            getEndpointTarget().asTypeEndpointTarget().getType().getTypeDecl());
+    if (!contextFreeEndpointsWithSameType.isEmpty()) {
+      return contextFreeEndpointsWithSameType.get(0);
+    }
+    return null;
+  }
 }
 
 aspect ErrorMessage {
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.jadd b/ragconnect.base/src/main/jastadd/Intermediate.jadd
index 3729453a4123d34514ad3475b439139bd7ef7136..a391130b5dd5991f1ceba4c0d2688a8bb17015a3 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.jadd
+++ b/ragconnect.base/src/main/jastadd/Intermediate.jadd
@@ -157,6 +157,14 @@ aspect MustacheMappingApplicationAndDefinition {
   eq MTypeSendDefinition.preemptiveExpectedValue() = lastValue();
   eq MTypeSendDefinition.preemptiveReturn() = "return false;";
 
+  eq MContextFreeTypeReceiveDefinition.firstInputVarName() = "message";
+  eq MContextFreeTypeReceiveDefinition.preemptiveExpectedValue() = "this";
+  eq MContextFreeTypeReceiveDefinition.preemptiveReturn() = "return;";
+
+  eq MContextFreeTypeSendDefinition.firstInputVarName() = null;
+  eq MContextFreeTypeSendDefinition.preemptiveExpectedValue() = null;
+  eq MContextFreeTypeSendDefinition.preemptiveReturn() = null;
+
   syn String MEndpointDefinition.parentTypeName() = getEndpointDefinition().parentTypeName();
 
   syn String MEndpointDefinition.getterMethodName() = getEndpointDefinition().getterMethodName();
@@ -174,9 +182,17 @@ aspect MustacheRagConnect {
 
   syn List<EndpointDefinition> RagConnect.allEndpointDefinitionList() {
     List<EndpointDefinition> result = new ArrayList<>();
-    for (ConnectSpecification spec : getConnectSpecificationFileList()) {
-      spec.getEndpointDefinitionList().forEach(result::add);
+    // first gather all user-defined endpoint definitions, that are not context-free
+    for (EndpointDefinition def : givenEndpointDefinitionList()) {
+      if (!def.hasContextFreeTypeEndpointTarget()) {
+        result.add(def);
+      }
+    }
+    // then check for additional endpoints, and add if no conflict with existing definitions exists
+    for (EndpointDefinition def : givenEndpointDefinitionList()) {
+      def.getEndpointTarget().impliedEndpointDefinitions().iterator().forEachRemaining(result::add);
     }
+
     return result;
   }
 
@@ -192,8 +208,54 @@ aspect MustacheRagConnect {
     return result;
   }
 
+  syn List<TypeDecl> RagConnect.typeDeclsOfContextFreeEndpointTargets() {
+    List<TypeDecl> result = new ArrayList<>();
+    for (EndpointTarget target : givenEndpointTargetList()) {
+      if (target.isContextFreeTypeEndpointTarget()) {
+        result.add(target.asContextFreeTypeEndpointTarget().getTypeDecl());
+      }
+    }
+    return result;
+  }
+
   // === MappingDefinition ===
   syn boolean MappingDefinition.isUsed() = !effectiveUsedAt().isEmpty();
+
+  // === attributes needed for computing above ones ===
+  syn List<EndpointDefinition> RagConnect.givenEndpointDefinitionList() {
+    List<EndpointDefinition> result = new ArrayList<>();
+    for (ConnectSpecification spec : getConnectSpecificationFileList()) {
+      spec.getEndpointDefinitionList().forEach(result::add);
+    }
+    return result;
+  }
+
+  syn nta JastAddList<EndpointDefinition> EndpointTarget.impliedEndpointDefinitions() = new JastAddList<>();
+  eq ContextFreeTypeEndpointTarget.impliedEndpointDefinitions() {
+    JastAddList<EndpointDefinition> result = super.impliedEndpointDefinitions();
+    EndpointDefinition containingDef = containingEndpointDefinition();
+    for (TypeComponent typeComponent : getTypeDecl().occurencesInProductionRules()) {
+      List<EndpointDefinition> defsForTypeComponent = lookupTypeEndpointDefinitions(typeComponent);
+      if (!defsForTypeComponent.stream().anyMatch(containingDef::matchesType)) {
+        // there is no user-defined endpoint definition for this typeComponent yet
+        // -> create a new endpoint definition with the same options and mappings as the context-free def
+        //    (except indexed-based for list-types)
+        EndpointDefinition newDef = new EndpointDefinition();
+        newDef.setAlwaysApply(containingDef.getAlwaysApply());
+        newDef.setIndexBasedListAccess(typeComponent.isListComponent());
+        newDef.setSend(containingDef.getSend());
+        containingDef.getMappings().forEach(newDef::addMapping);
+
+        TypeEndpointTarget target = new TypeEndpointTarget();
+        target.setType(typeComponent);
+        newDef.setEndpointTarget(target);
+
+        result.add(newDef);
+      }
+    }
+    return result;
+  }
+
 }
 
 aspect MustacheReceiveAndSendAndHandleUri {
@@ -234,6 +296,10 @@ aspect MustacheReceiveAndSendAndHandleUri {
   eq TypeEndpointTarget.parentTypeName() = getType().containingTypeDecl().getName();
   eq TypeEndpointTarget.entityName() = getType().getName() + (typeIsList() && !containingEndpointDefinition().getIndexBasedListAccess() ? "List" : "");
 
+  eq ContextFreeTypeEndpointTarget.getterMethodName() = null;
+  eq ContextFreeTypeEndpointTarget.parentTypeName() = getTypeDecl().getName();
+  eq ContextFreeTypeEndpointTarget.entityName() = "";
+
 }
 
 aspect MustacheReceiveDefinition {
@@ -241,6 +307,8 @@ aspect MustacheReceiveDefinition {
   syn boolean RagConnect.configLoggingEnabledForReads() = getConfiguration().getLoggingEnabledForReads();
 
   // === EndpointDefinition ===
+  syn boolean EndpointDefinition.hasContextFreeTypeEndpointTarget() = getEndpointTarget().isContextFreeTypeEndpointTarget();
+
   syn boolean EndpointDefinition.hasTypeEndpointTarget() = getEndpointTarget().isTypeEndpointTarget();
 
   syn String EndpointDefinition.idTokenName() = "InternalRagconnectTopicInList";
@@ -281,26 +349,29 @@ aspect MustacheSendDefinition {
   syn String EndpointTarget.senderName();
   eq TokenEndpointTarget.senderName() = "_sender_" + getToken().getName();
   eq TypeEndpointTarget.senderName() = "_sender_" + getType().getName();
+  eq ContextFreeTypeEndpointTarget.senderName() = null;
 
   syn String MEndpointDefinition.updateMethodName();
   syn String MEndpointDefinition.writeMethodName();
 
-  // MTokenReceiveDefinition
   eq MTokenReceiveDefinition.updateMethodName() = null;
   eq MTokenReceiveDefinition.writeMethodName() = null;
 
-  // MTokenSendDefinition
   eq MTokenSendDefinition.updateMethodName() = "_update_" + tokenName();
   eq MTokenSendDefinition.writeMethodName() = "_writeLastValue_" + tokenName();
 
-  // MTypeReceiveDefinition
   eq MTypeReceiveDefinition.updateMethodName() = null;
   eq MTypeReceiveDefinition.writeMethodName() = null;
 
-  // MTypeSendDefinition
   eq MTypeSendDefinition.updateMethodName() = "_update_" + typeName();
   eq MTypeSendDefinition.writeMethodName() = "_writeLastValue_" + typeName();
 
+  eq MContextFreeTypeReceiveDefinition.updateMethodName() = null;
+  eq MContextFreeTypeReceiveDefinition.writeMethodName() = null;
+
+  eq MContextFreeTypeSendDefinition.updateMethodName() = null;
+  eq MContextFreeTypeSendDefinition.writeMethodName() = null;
+
   syn String EndpointDefinition.tokenName() = token().getName();
   syn String MEndpointDefinition.tokenName() = getEndpointDefinition().tokenName();
 
@@ -350,6 +421,25 @@ aspect MustacheTokenComponent {
   // > see MustacheSend for updateMethodName, writeMethodName
 }
 
+aspect MustacheTypeDecl {
+  // === TypeComponent ===
+  syn String TypeComponent.parentTypeName() = containingTypeDecl().getName();
+
+  // === TypeDecl ===
+  syn List<TypeComponent> TypeDecl.occurencesInProductionRules() {
+    List<TypeComponent> result = new ArrayList<>();
+    for (TypeDecl typeDecl : program().typeDecls()) {
+      for (Component comp : typeDecl.getComponentList()) {
+        if (comp.isTypeComponent() && comp.asTypeComponent().getTypeDecl().equals(this)) {
+          result.add(comp.asTypeComponent());
+        }
+      }
+    }
+    return result;
+  }
+
+}
+
 aspect AttributesForMustache {
   syn String MEndpointDefinition.lastValue() = getEndpointDefinition().lastValue();
 
@@ -361,22 +451,7 @@ aspect AttributesForMustache {
   syn MInnerMappingDefinition MEndpointDefinition.lastDefinition() = getInnerMappingDefinition(getNumInnerMappingDefinition() - 1);
 
   syn nta MEndpointDefinition EndpointDefinition.toMustache() {
-    final MEndpointDefinition result;
-    if (getEndpointTarget().isTokenEndpointTarget()) {
-      if (getSend()) {
-        result = new MTokenSendDefinition();
-      } else {
-        result = new MTokenReceiveDefinition();
-      }
-    } else if (getEndpointTarget().isTypeEndpointTarget()) {
-      if (getSend()) {
-        result = new MTypeSendDefinition();
-      } else {
-        result = new MTypeReceiveDefinition();
-      }
-    } else {
-      throw new RuntimeException("Unknown endpoint target type: " + getEndpointTarget());
-    }
+    final MEndpointDefinition result = getEndpointTarget().createMEndpointDefinition(getSend());
     result.setEndpointDefinition(this);
     for (MappingDefinition def : effectiveMappings()) {
       MInnerMappingDefinition inner = new MInnerMappingDefinition();
@@ -385,6 +460,20 @@ aspect AttributesForMustache {
     }
     return result;
   }
+  abstract MEndpointDefinition EndpointTarget.createMEndpointDefinition(boolean isSend);
+  MEndpointDefinition TokenEndpointTarget.createMEndpointDefinition(boolean isSend) {
+    return isSend ? new MTokenSendDefinition() : new MTokenReceiveDefinition();
+  }
+  MEndpointDefinition TypeEndpointTarget.createMEndpointDefinition(boolean isSend) {
+    return isSend ? new MTypeSendDefinition() : new MTypeReceiveDefinition();
+  }
+  MEndpointDefinition ContextFreeTypeEndpointTarget.createMEndpointDefinition(boolean isSend) {
+    return isSend ? new MContextFreeTypeSendDefinition() : new MContextFreeTypeReceiveDefinition();
+  }
+  MEndpointDefinition UntypedEndpointTarget.createMEndpointDefinition(boolean isSend) {
+    throw new RuntimeException("Untyped endpoint target type, typeName= " +
+        getTypeName() + ", childName=" + getChildName());
+  }
 }
 
 aspect GrammarGeneration {
diff --git a/ragconnect.base/src/main/jastadd/Intermediate.relast b/ragconnect.base/src/main/jastadd/Intermediate.relast
index d7afd2ea942558f6999c8fa64c91bfdc480d659c..79097503ef32b417be179ccb5f679886a6297582 100644
--- a/ragconnect.base/src/main/jastadd/Intermediate.relast
+++ b/ragconnect.base/src/main/jastadd/Intermediate.relast
@@ -7,6 +7,9 @@ MTokenSendDefinition : MTokenEndpointDefinition;
 abstract MTypeEndpointDefinition : MEndpointDefinition;
 MTypeReceiveDefinition : MTypeEndpointDefinition;
 MTypeSendDefinition : MTypeEndpointDefinition;
+abstract MContextFreeTypeEndpointDefinition : MEndpointDefinition;
+MContextFreeTypeReceiveDefinition : MContextFreeTypeEndpointDefinition;
+MContextFreeTypeSendDefinition : MContextFreeTypeEndpointDefinition;
 
 MInnerMappingDefinition;
 rel MInnerMappingDefinition.MappingDefinition -> MappingDefinition;
diff --git a/ragconnect.base/src/main/jastadd/Mappings.jrag b/ragconnect.base/src/main/jastadd/Mappings.jrag
index 093f4cc1d4f1cb0eecf1ffe7914c20e6fa715b27..45d28e8d99c02292780fed005642e72489a1c58e 100644
--- a/ragconnect.base/src/main/jastadd/Mappings.jrag
+++ b/ragconnect.base/src/main/jastadd/Mappings.jrag
@@ -294,6 +294,7 @@ aspect Mappings {
   syn String EndpointTarget.targetTypeName();
   eq TokenEndpointTarget.targetTypeName() = getToken().effectiveJavaTypeUse().getName();
   eq TypeEndpointTarget.targetTypeName() = getType().getTypeDecl().getName();
+  eq ContextFreeTypeEndpointTarget.targetTypeName() = getTypeDecl().getName();
 
   //  eq ReceiveFromRestDefinition.suitableDefaultMapping() {
   //    String typeName = getMappingList().isEmpty() ?
diff --git a/ragconnect.base/src/main/jastadd/NameResolution.jrag b/ragconnect.base/src/main/jastadd/NameResolution.jrag
index 9b37ee7f93ea9f31414f61a3a70b01a1ba5ca489..cf29d470e5734409b9cc8debd48ef70c3e281638 100644
--- a/ragconnect.base/src/main/jastadd/NameResolution.jrag
+++ b/ragconnect.base/src/main/jastadd/NameResolution.jrag
@@ -5,7 +5,7 @@ aspect RagConnectNameResolution {
   eq RagConnect.getConnectSpecificationFile().lookupTokenEndpointDefinitions(TokenComponent token) = lookupTokenEndpointDefinitions(token);
   syn java.util.List<EndpointDefinition> RagConnect.lookupTokenEndpointDefinitions(TokenComponent token) {
     java.util.List<EndpointDefinition> result = new java.util.ArrayList<>();
-    for (EndpointTarget target : allEndpointTargetList()) {
+    for (EndpointTarget target : givenEndpointTargetList()) {
       if (target.isTokenEndpointTarget() && target.asTokenEndpointTarget().getToken().equals(token)) {
         result.add(target.containingEndpointDefinition());
       }
@@ -19,7 +19,7 @@ aspect RagConnectNameResolution {
   eq RagConnect.getConnectSpecificationFile().lookupTypeEndpointDefinitions(TypeComponent type) = lookupTypeEndpointDefinitions(type);
   syn java.util.List<EndpointDefinition> RagConnect.lookupTypeEndpointDefinitions(TypeComponent type) {
     java.util.List<EndpointDefinition> result = new java.util.ArrayList<>();
-    for (EndpointTarget target : allEndpointTargetList()) {
+    for (EndpointTarget target : givenEndpointTargetList()) {
       if (target.isTypeEndpointTarget() && target.asTypeEndpointTarget().getType().equals(type)) {
         result.add(target.containingEndpointDefinition());
       }
@@ -27,6 +27,20 @@ aspect RagConnectNameResolution {
     return result;
   }
 
+  // --- lookupContextFreeTypeEndpointDefinition ---
+  inh java.util.List<EndpointDefinition> EndpointDefinition.lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl);
+  inh java.util.List<EndpointDefinition> EndpointTarget.lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl);
+  eq RagConnect.getConnectSpecificationFile().lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl) = lookupContextFreeTypeEndpointDefinitions(typeDecl);
+  syn java.util.List<EndpointDefinition> RagConnect.lookupContextFreeTypeEndpointDefinitions(TypeDecl typeDecl) {
+    java.util.List<EndpointDefinition> result = new java.util.ArrayList<>();
+    for (EndpointTarget target : givenEndpointTargetList()) {
+      if (target.isContextFreeTypeEndpointTarget() && target.asContextFreeTypeEndpointTarget().getTypeDecl().equals(typeDecl)) {
+        result.add(target.containingEndpointDefinition());
+      }
+    }
+    return result;
+  }
+
   // --- lookupDependencyDefinition ---
   inh DependencyDefinition DependencyDefinition.lookupDependencyDefinition(TypeDecl source, String id);
   eq RagConnect.getConnectSpecificationFile().lookupDependencyDefinition(TypeDecl source, String id) {
diff --git a/ragconnect.base/src/main/jastadd/Navigation.jrag b/ragconnect.base/src/main/jastadd/Navigation.jrag
index 7419f3d880b592e28ae4a45b7a9451849b855368..a65ab72fa946bcec3351bb496f6632c012bac678 100644
--- a/ragconnect.base/src/main/jastadd/Navigation.jrag
+++ b/ragconnect.base/src/main/jastadd/Navigation.jrag
@@ -12,6 +12,12 @@ aspect NewStuff {
   syn boolean EndpointTarget.isTypeEndpointTarget() = false;
   eq TypeEndpointTarget.isTypeEndpointTarget() = true;
 
+  /** Tests if EndpointTarget is a ContextFreeTypeEndpointTarget.
+  *  @return 'true' if this is a ContextFreeTypeEndpointTarget, otherwise 'false'
+  */
+  syn boolean EndpointTarget.isContextFreeTypeEndpointTarget() = false;
+  eq ContextFreeTypeEndpointTarget.isContextFreeTypeEndpointTarget() = true;
+
   /** Tests if EndpointTarget is a UntypedEndpointTarget.
   *  @return 'true' if this is a UntypedEndpointTarget, otherwise 'false'
   */
@@ -32,6 +38,13 @@ aspect NewStuff {
   eq EndpointTarget.asTypeEndpointTarget() = null;
   eq TypeEndpointTarget.asTypeEndpointTarget() = this;
 
+  /** casts a EndpointTarget into a ContextFreeTypeEndpointTarget if possible.
+   *  @return 'this' cast to a ContextFreeTypeEndpointTarget or 'null'
+   */
+  syn ContextFreeTypeEndpointTarget EndpointTarget.asContextFreeTypeEndpointTarget();
+  eq EndpointTarget.asContextFreeTypeEndpointTarget() = null;
+  eq ContextFreeTypeEndpointTarget.asContextFreeTypeEndpointTarget() = this;
+
   /** casts a EndpointTarget into a UntypedEndpointTarget if possible.
    *  @return 'this' cast to a UntypedEndpointTarget or 'null'
    */
@@ -73,8 +86,8 @@ aspect RagConnectNavigation {
   }
 
 
-  //--- allEndpointTargetList ---
-  syn List<EndpointTarget> RagConnect.allEndpointTargetList() {
+  //--- givenEndpointTargetList ---
+  syn List<EndpointTarget> RagConnect.givenEndpointTargetList() {
     List<EndpointTarget> result = new ArrayList<>();
     for (ConnectSpecification spec : getConnectSpecificationFileList()) {
       spec.getEndpointDefinitionList().forEach(endpointDef -> result.add(endpointDef.getEndpointTarget()));
diff --git a/ragconnect.base/src/main/jastadd/RagConnect.relast b/ragconnect.base/src/main/jastadd/RagConnect.relast
index 30e6d5ad6a034f9b35de570414e8d4e904751ca2..37ec5feaecbff82163d7ca1360c9916abfaed36e 100644
--- a/ragconnect.base/src/main/jastadd/RagConnect.relast
+++ b/ragconnect.base/src/main/jastadd/RagConnect.relast
@@ -11,7 +11,9 @@ TokenEndpointTarget : EndpointTarget;
 rel TokenEndpointTarget.Token <-> TokenComponent.TokenEndpointTarget*;
 TypeEndpointTarget : EndpointTarget;
 rel TypeEndpointTarget.Type <-> TypeComponent.TypeEndpointTarget*;
-UntypedEndpointTarget : EndpointTarget ::= <TokenOrType>;  // only used by parser
+ContextFreeTypeEndpointTarget : EndpointTarget;
+rel ContextFreeTypeEndpointTarget.TypeDecl <-> TypeDecl.ContextFreeTypeEndpointTarget?;
+UntypedEndpointTarget : EndpointTarget ::= <TypeName> <ChildName>;  // only used by parser
 // to be integrated:
 //AttributeEndpointTarget : EndpointTarget ::= <Name> ;
 //RelationEndpointTarget : EndpointTarget ;
diff --git a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
index bbf0c11b7ba08c1f7c172cf8fd31420e7fd74561..23dbabaef4ce3d4e0ce114591d1c4ee61937b878 100644
--- a/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
+++ b/ragconnect.base/src/main/jastadd/parser/ParserRewrites.jrag
@@ -1,19 +1,37 @@
 aspect ParserRewrites {
   rewrite UntypedEndpointTarget {
-    when (tryGloballyResolveTypeComponentByToken(getTokenOrType()) != null)
+    when (getChildName() != null && tryGloballyResolveTypeComponentByToken(combinedName()) != null)
     to TypeEndpointTarget {
       TypeEndpointTarget result = new TypeEndpointTarget();
-      result.setType(TypeComponent.createRef(this.getTokenOrType()));
+      result.copyOtherValuesFrom(this);
+      result.setType(TypeComponent.createRef(this.combinedName()));
       return result;
     }
-    when (tryGloballyResolveTokenComponentByToken(getTokenOrType()) != null)
+
+    when (getChildName() != null && tryGloballyResolveTokenComponentByToken(combinedName()) != null)
     to TokenEndpointTarget {
       TokenEndpointTarget result = new TokenEndpointTarget();
-      result.setToken(TokenComponent.createRef(this.getTokenOrType()));
+      result.copyOtherValuesFrom(this);
+      result.setToken(TokenComponent.createRef(this.combinedName()));
+      return result;
+    }
+
+    when (getChildName() == "")
+    to ContextFreeTypeEndpointTarget {
+      ContextFreeTypeEndpointTarget result = new ContextFreeTypeEndpointTarget();
+      result.copyOtherValuesFrom(this);
+      result.setTypeDecl(TypeDecl.createRef(getTypeName()));
       return result;
     }
   }
 
+  syn String UntypedEndpointTarget.combinedName() = getTypeName() + "." + getChildName();
+
+  protected void EndpointTarget.copyOtherValuesFrom(EndpointTarget source) {
+    this.setStart(source.getStartLine(), source.getStartColumn());
+    this.setEnd(source.getEndLine(), source.getEndColumn());
+  }
+
   eq UntypedEndpointTarget.senderName() = "<untyped.senderName>";
   eq UntypedEndpointTarget.getterMethodName() = "<untyped.getterMethodName>";
   eq UntypedEndpointTarget.parentTypeName() = "<untyped.parentTypeName>";
diff --git a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
index 5ddef630253006a13e64ea84cd4df30dc4b6aa2b..2c326763856f437e770e9bf5ba1566c86f2a2fdb 100644
--- a/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
+++ b/ragconnect.base/src/main/jastadd/parser/RagConnect.parser
@@ -27,13 +27,13 @@ ConnectSpecificationFile connect_specification_file
 //    return def;
 //  }
   private EndpointDefinition createEndpointDefinition(
-    String type_name, String child_name, boolean send,
+    EndpointTarget endpointTarget, boolean send,
     boolean indexBasedListAccess, boolean withAdd) {
       EndpointDefinition result = new EndpointDefinition();
       result.setSend(send);
       result.setIndexBasedListAccess(indexBasedListAccess);
       result.setWithAdd(withAdd);
-      result.setEndpointTarget(new UntypedEndpointTarget(type_name + "." + child_name));
+      result.setEndpointTarget(endpointTarget);
       return result;
     }
 :} ;
@@ -53,30 +53,16 @@ EndpointDefinition endpoint_definition
 ;
 
 EndpointDefinition endpoint_definition_type
-  = SEND ID.type_name DOT ID.child_name
-    {:
-      return createEndpointDefinition(type_name, child_name, true, false, false);
-    :}
-//  | SEND INDEXED ID.type_name DOT ID.child_name
-//    {:
-//      return createEndpointDefinition(type_name, child_name, true, true, false);
-//    :}
-  | RECEIVE ID.type_name DOT ID.child_name
-    {:
-      return createEndpointDefinition(type_name, child_name, false, false, false);
-    :}
-  | RECEIVE INDEXED ID.type_name DOT ID.child_name
-    {:
-      return createEndpointDefinition(type_name, child_name, false, true, false);
-    :}
-  | RECEIVE WITH ADD ID.type_name DOT ID.child_name
-    {:
-      return createEndpointDefinition(type_name, child_name, false, false, true);
-    :}
-  | RECEIVE INDEXED WITH ADD ID.type_name DOT ID.child_name
-    {:
-      return createEndpointDefinition(type_name, child_name, false, true, true);
-    :}
+  = SEND endpoint_target.t                        {: return createEndpointDefinition(t, true,  false, false); :}
+  | RECEIVE endpoint_target.t                     {: return createEndpointDefinition(t, false, false, false); :}
+  | RECEIVE INDEXED endpoint_target.t             {: return createEndpointDefinition(t, false, true,  false); :}
+  | RECEIVE WITH ADD endpoint_target.t            {: return createEndpointDefinition(t, false, false, true ); :}
+  | RECEIVE INDEXED WITH ADD endpoint_target.t    {: return createEndpointDefinition(t, false, true,  true ); :}
+;
+
+EndpointTarget endpoint_target
+  = ID.type_name DOT ID.child_name    {: return new UntypedEndpointTarget(type_name, child_name); :}
+  | ID.type_name                      {: return new UntypedEndpointTarget(type_name, ""); :}
 ;
 
 ArrayList string_list
diff --git a/ragconnect.base/src/main/resources/ragconnect.mustache b/ragconnect.base/src/main/resources/ragconnect.mustache
index c3b50710f38fd247aca8ca11456c23279a0bee8c..d39fe4a8686b7a6950c1bfb7128d0960f8fa8349 100644
--- a/ragconnect.base/src/main/resources/ragconnect.mustache
+++ b/ragconnect.base/src/main/resources/ragconnect.mustache
@@ -28,6 +28,10 @@ aspect RagConnect {
   {{> tokenComponent}}
   {{/tokenComponentsThatNeedProxy}}
 
+  {{#typeDeclsOfContextFreeEndpointTargets}}
+  {{> typeDecl}}
+  {{/typeDeclsOfContextFreeEndpointTargets}}
+
   {{> ListAspect}}
 
   public void {{rootNodeName}}.ragconnectCheckIncremental() {
diff --git a/ragconnect.base/src/main/resources/typeDecl.mustache b/ragconnect.base/src/main/resources/typeDecl.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..643fbc80a4eccfa50da5d48650e03d4380badb62
--- /dev/null
+++ b/ragconnect.base/src/main/resources/typeDecl.mustache
@@ -0,0 +1,45 @@
+uncache {{Name}}._ragconnect_myContext();
+inh String {{Name}}._ragconnect_myContext();
+inh ASTNode {{Name}}._ragconnect_myParent();
+uncache {{Name}}._ragconnect_myIndexInList();
+inh int {{Name}}._ragconnect_myIndexInList();
+{{#occurencesInProductionRules}}
+  eq {{parentTypeName}}.get{{Name}}()._ragconnect_myContext() = "{{parentTypeName}}.{{Name}}";
+  eq {{parentTypeName}}.get{{Name}}()._ragconnect_myParent() = this;
+  {{^isListComponent}}
+  eq {{parentTypeName}}.get{{Name}}()._ragconnect_myIndexInList() = -1;
+  {{/isListComponent}}
+  {{#isListComponent}}
+  eq {{parentTypeName}}.get{{Name}}(int i)._ragconnect_myIndexInList() = i;
+  {{/isListComponent}}
+{{/occurencesInProductionRules}}
+
+{{#ContextFreeTypeEndpointTarget}}{{#containingEndpointDefinition}}
+public boolean {{Name}}.{{connectMethodName}}(String {{connectParameterName}}) throws java.io.IOException {
+  switch (_ragconnect_myContext()) {
+  {{#occurencesInProductionRules}}
+    {{!only using "connectMethodName" is not correct, since the actual name might be different, e.g., if both send and receive are defined. need a reference to the actual endpoint-definition here}}
+    {{^isListComponent}}
+    case "{{parentTypeName}}.{{Name}}": return (({{parentTypeName}}) _ragconnect_myParent()).{{connectMethodName}}{{Name}}({{connectParameterName}});
+    {{/isListComponent}}
+    {{#isListComponent}}
+    case "{{parentTypeName}}.{{Name}}": return (({{parentTypeName}}) _ragconnect_myParent()).{{connectMethodName}}{{Name}}({{connectParameterName}}, _ragconnect_myIndexInList());
+    {{/isListComponent}}
+  {{/occurencesInProductionRules}}
+    default:
+      System.err.println("No matching context while connecting " + this + " to " + {{connectParameterName}});
+      return false;
+  }
+}
+
+public boolean {{Name}}.{{disconnectMethodName}}(String {{connectParameterName}}) throws java.io.IOException {
+  switch (_ragconnect_myContext()) {
+{{#occurencesInProductionRules}}
+  case "{{parentTypeName}}.{{Name}}": return (({{parentTypeName}}) _ragconnect_myParent()).{{disconnectMethodName}}{{Name}}({{connectParameterName}});
+{{/occurencesInProductionRules}}
+    default:
+      System.err.println("No matching context while disconnecting for " + this + " from " + {{connectParameterName}});
+      return false;
+  }
+}
+{{/containingEndpointDefinition}}{{/ContextFreeTypeEndpointTarget}}
diff --git a/ragconnect.tests/build.gradle b/ragconnect.tests/build.gradle
index cfeb6ae4680eca67212c256e3e39debc514609c7..865abd9e99d657441dde62ba38112f0e3b18604f 100644
--- a/ragconnect.tests/build.gradle
+++ b/ragconnect.tests/build.gradle
@@ -105,13 +105,22 @@ task specificTest(type: Test, dependsOn: testClasses) {
     description = 'Run test tagged with tag given by "-PincludeTags="'
     group = 'verification'
     String tags = project.hasProperty("includeTags") ?
-                   project.property("includeTags") : ''
+            project.property("includeTags") : ''
 
     useJUnitPlatform {
         includeTags tags
     }
 }
 
+task newTests(type: Test, dependsOn: testClasses) {
+    description = 'Run test tagged with tag "New"'
+    group = 'verification'
+
+    useJUnitPlatform {
+        includeTags 'New'
+    }
+}
+
 preprocessorTesting {
     //noinspection GroovyAssignabilityCheck
     relastCompilerLocation = '../libs/relast.jar'
@@ -556,21 +565,27 @@ task compileSingleListVariantIncremental(type: RagConnectTest, dependsOn: ':ragc
     }
 }
 
-task cleanCurrentManualTest(type: Delete) {
-//    delete "src/test/02-after-ragconnect/singleListVariant"
-//    delete "src/test/03-after-relast/singleListVariant"
-//    delete "src/test/java-gen/singleListVariant/ast"
-    delete "src/test/02-after-ragconnect/singleList"
-    delete "src/test/03-after-relast/singleList"
-    delete "src/test/java-gen/singleList/ast"
-}
-task cleanCurrentIncrementalTest(type: Delete) {
-//    delete "src/test/02-after-ragconnect/singleListVariantInc"
-//    delete "src/test/03-after-relast/singleListVariantInc"
-//    delete "src/test/java-gen/singleListVariantInc/ast"
-    delete "src/test/02-after-ragconnect/singleListInc"
-    delete "src/test/03-after-relast/singleListInc"
-    delete "src/test/java-gen/singleListInc/ast"
+// --- Test: contextFreeSimple-incremental ---
+task compileContextFreeSimpleIncremental(type: RagConnectTest, dependsOn: ':ragconnect.base:jar') {
+    ragconnect {
+        outputDir = file('src/test/02-after-ragconnect/contextFreeSimpleInc')
+        inputFiles = [file('src/test/01-input/contextFreeSimple/Test.relast'),
+                      file('src/test/01-input/contextFreeSimple/Test.connect')]
+        rootNode = 'Root'
+        extraOptions = ['--experimental-jastadd-329']
+    }
+    relast {
+        useJastAddNames = true
+        grammarName = 'src/test/03-after-relast/contextFreeSimpleInc/contextFreeSimpleInc'
+        serializer = 'jackson'
+    }
+    jastadd {
+        jastAddList = 'JastAddList'
+        packageName = 'contextFreeSimpleInc.ast'
+        extraOptions = ['--tracing=cache,flush',
+                        '--incremental=param',
+                        '--cache=all',
+                        '--rewrite=cnta',
+                        '--flush=full']
+    }
 }
-compileSingleListManual.dependsOn cleanCurrentManualTest
-compileSingleListIncremental.dependsOn cleanCurrentIncrementalTest
diff --git a/ragconnect.tests/src/test/01-input/contextFreeSimple/README.md b/ragconnect.tests/src/test/01-input/contextFreeSimple/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..737ca9af132d047a1cceafee2e6b1d2a0552a6b7
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/contextFreeSimple/README.md
@@ -0,0 +1,3 @@
+# ContextFree-Simple
+
+Idea: Use only context-free context, in simple situation (non-recursive)
diff --git a/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.connect b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.connect
new file mode 100644
index 0000000000000000000000000000000000000000..7a6aeab676953fdba30bb04f111c7b1fd6bc0655
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.connect
@@ -0,0 +1,18 @@
+receive A;
+
+//"receive Root.A;" is implied
+receive Root.SingleA using PrependPrefix;
+receive Root.OptA using AddSuffix;
+//"receive Root.ListA;" would clash as "receive indexed Root.ListA" is implied
+
+PrependPrefix maps A a to A {:
+  A result = new A();
+  result.setValue("pre" + a.getValue());
+  return result;
+:}
+
+AddSuffix maps A a to A {:
+  A result = new A();
+  result.setValue(a.getValue() + "post");
+  return result;
+:}
diff --git a/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.relast b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.relast
new file mode 100644
index 0000000000000000000000000000000000000000..f5dff3f8b742bc18cfbad715b1326ce483ce8a25
--- /dev/null
+++ b/ragconnect.tests/src/test/01-input/contextFreeSimple/Test.relast
@@ -0,0 +1,2 @@
+Root ::= A SingleA:A [OptA:A] ListA:A* ;
+A ::= <Value> ;
diff --git a/ragconnect.tests/src/test/01-input/errors/Errors.relast b/ragconnect.tests/src/test/01-input/errors/Errors.relast
index fcde7df1d2e48b9591ddea31aaa3b0a9efdd4cfa..f700f39abdcac18174e192bfc6098d95e4720573 100644
--- a/ragconnect.tests/src/test/01-input/errors/Errors.relast
+++ b/ragconnect.tests/src/test/01-input/errors/Errors.relast
@@ -1,10 +1,10 @@
-A ::= B C D ;
+A ::= B C D E* ;
 
 // read definitions
 B ::= /<ErrorNTA:String>/ <ErrorTypeOfFirstMapping:String> <ErrorTypeOfLastMapping:String> <DoubledValue:int> <ErrorTypeMismatch:String> ;
 
 // write definitions
-C ::= <ErrorNotNTA:String> /<ErrorTypeOfFirstMapping:String>/ /<ErrorTypeOfLastMapping1:String>/ /<ErrorTypeOfLastMapping2:List<String>>/ /<ErrorTypeMismatch:String>/ /<DoubledValue:int>/ ;
+C ::= /<ErrorTypeOfFirstMapping:String>/ /<ErrorTypeOfLastMapping1:String>/ /<ErrorTypeOfLastMapping2:List<String>>/ /<ErrorTypeMismatch:String>/ /<DoubledValue:int>/ ;
 
 // dependency definitions
 D ::= <SourceNonExistingTarget>
@@ -13,3 +13,6 @@ D ::= <SourceNonExistingTarget>
       <SourceSameAsListNode> /<TargetSameAsListNode>/
       <SourceDoubledValue> /<TargetDoubledValue>/
       MyList:D* ;
+
+// context-free endpoints
+E ::= ;
diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.connect b/ragconnect.tests/src/test/01-input/errors/Standard.connect
index bd57822a6c70198cc36698f790fa90655ed4db73..844c2e97b4f6e32e0e1f3f1392065a2f0ea3efbc 100644
--- a/ragconnect.tests/src/test/01-input/errors/Standard.connect
+++ b/ragconnect.tests/src/test/01-input/errors/Standard.connect
@@ -1,4 +1,4 @@
-// --- update receive definitions ---
+// --- receive definitions ---
 // Error: there must not be two receive definitions for the same token
 receive B.DoubledValue ;
 receive B.DoubledValue using IntToInt ;
@@ -18,22 +18,26 @@ receive B.ErrorTypeOfLastMapping using StringToList ;
 // Error: types of mappings must match (modulo inheritance)
 receive B.ErrorTypeMismatch using StringToList, IntToInt ;
 
-// --- update send definitions ---
+// Error: Context-Free endpoint not allowed for root nodes
+receive A;
+
+// Error: Clash with implied endpoint definition of context-free endpoint
+receive E;
+receive A.E;
+
+// --- send definitions ---
 // NOT HANDLED \\ Error: the token must be resolvable within the parent type
 // NOT HANDLED \\ receive C.NonExisting ;
 
-// Error: Token must be a TokenNTA (i.e., check for Token.getNTA())
-send C.ErrorNotNTA ;
+// NOT HANDLED \\ // Error: from-type of first mapping must be type of Token
+// NOT HANDLED \\ send C.ErrorTypeOfFirstMapping using IntToInt ;
 
-// Error: from-type of first mapping must be type of Token
-send C.ErrorTypeOfFirstMapping using IntToInt ;
+// NOT HANDLED \\ // Error: to-type of last mapping must be byte[] or a supported primitive type
+// NOT HANDLED \\ send C.ErrorTypeOfLastMapping1 using StringToList ;
+// NOT HANDLED \\ send C.ErrorTypeOfLastMapping2 ;
 
-// Error: to-type of last mapping must be byte[] or a supported primitive type
-send C.ErrorTypeOfLastMapping1 using StringToList ;
-send C.ErrorTypeOfLastMapping2 ;
-
-// Error: types of mappings must match (modulo inheritance)
-send C.ErrorTypeMismatch using StringToList, IntToInt ;
+// NOT HANDLED \\ // Error: types of mappings must match (modulo inheritance)
+// NOT HANDLED \\ send C.ErrorTypeMismatch using StringToList, IntToInt ;
 
 // Error: no more than one send mapping for each TokenComponent
 send C.DoubledValue ;
@@ -44,8 +48,8 @@ send C.DoubledValue using IntToInt ;
 // NOT HANDLED \\ D.SourceNonExistingTarget canDependOn D.NonExisting as NonExistingTarget ;
 // NOT HANDLED \\ D.NonExisting canDependOn D.TargetNonExistingSource as NonExistingSource ;
 
-// Error: There must be a send update definition for the target token
-D.SourceNoWriteDef canDependOn D.TargetNoWriteDef as NoWriteDef ;
+// NOT HANDLED \\ // Error: There must be a send update definition for the target token
+// NOT HANDLED \\ D.SourceNoWriteDef canDependOn D.TargetNoWriteDef as NoWriteDef ;
 
 // Error: The name of a dependency definition must not be equal to a list-node on the source
 D.SourceSameAsListNode canDependOn D.TargetSameAsListNode as MyList ;
diff --git a/ragconnect.tests/src/test/01-input/errors/Standard.expected b/ragconnect.tests/src/test/01-input/errors/Standard.expected
index 2e14612cc9fc4d7ffae435745e8b8593a22258d5..d258c2c599b54523d4ecff92eb95af8f419a9ec9 100644
--- a/ragconnect.tests/src/test/01-input/errors/Standard.expected
+++ b/ragconnect.tests/src/test/01-input/errors/Standard.expected
@@ -5,7 +5,9 @@ Standard.connect Line 13, column 1: No suitable default mapping found for type j
 Standard.connect Line 13, column 1: to-type of last mapping (java.util.List) not assignable to type of the token (String)!
 Standard.connect Line 16, column 1: to-type of last mapping (List) not assignable to type of the token (String)!
 Standard.connect Line 19, column 1: to-type of last mapping (int) not assignable to type of the token (String)!
-Standard.connect Line 39, column 1: Endpoint definition already defined for C.DoubledValue
-Standard.connect Line 40, column 1: Endpoint definition already defined for C.DoubledValue
-Standard.connect Line 51, column 1: The name of a dependency definition must not be equal to a list-node on the source
-Standard.connect Line 56, column 1: Dependency definition already defined for D with name DoubledValue
+Standard.connect Line 22, column 9: Context-Free endpoint not allowed for root node A!
+Standard.connect Line 26, column 1: Clash with implied, indexed endpoint definition of context-free endpoint in line 25!
+Standard.connect Line 43, column 1: Endpoint definition already defined for C.DoubledValue
+Standard.connect Line 44, column 1: Endpoint definition already defined for C.DoubledValue
+Standard.connect Line 55, column 1: The name of a dependency definition must not be equal to a list-node on the source
+Standard.connect Line 60, column 1: Dependency definition already defined for D with name DoubledValue
diff --git a/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ContextFreeSimpleTest.java b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ContextFreeSimpleTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..afd6c59ed3a506dd5944f02b986bb6bbc6f32c4b
--- /dev/null
+++ b/ragconnect.tests/src/test/java/org/jastadd/ragconnect/tests/ContextFreeSimpleTest.java
@@ -0,0 +1,168 @@
+package org.jastadd.ragconnect.tests;
+
+import contextFreeSimpleInc.ast.A;
+import contextFreeSimpleInc.ast.Root;
+import contextFreeSimpleInc.ast.SerializationException;
+import org.junit.jupiter.api.Tag;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import static org.jastadd.ragconnect.tests.TestUtils.mqttUri;
+import static org.jastadd.ragconnect.tests.TestUtils.waitForMqtt;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Test case "context free simple".
+ *
+ * @author rschoene - Initial contribution
+ */
+@Tag("Incremental")
+public class ContextFreeSimpleTest extends AbstractMqttTest {
+
+  private static final String TOPIC_UNNAMED = "unnamed";
+  private static final String TOPIC_SINGLE = "single";
+  private static final String TOPIC_SINGLE_ALTERNATIVE = "double";
+  private static final String TOPIC_OPT = "opt";
+  private static final String TOPIC_LIST_1 = "list1";
+  private static final String TOPIC_LIST_2 = "list2";
+
+  /** Use initially created members as values in {@link #check(String, String, String, String, String)} */
+  private static final String INITIAL_VALUE = null;
+
+  private Root model;
+  private A unnamedA;
+  private A singleA;
+  private A optA;
+  private A listA1;
+  private A listA2;
+
+  @Override
+  protected void createModel() {
+    model = new Root();
+    unnamedA = new A().setValue("unnamed");
+    singleA = new A().setValue("single");
+    optA = new A().setValue("opt");
+    listA1 = new A().setValue("a1");
+    listA2 = new A().setValue("a2");
+
+    model.setA(unnamedA);
+    model.setSingleA(singleA);
+    model.setOptA(optA);
+    model.addListA(listA1);
+    model.addListA(listA2);
+  }
+
+  @Override
+  protected void setupReceiverAndConnect(boolean writeCurrentValue) throws IOException, InterruptedException {
+    model.ragconnectSetupMqttWaitUntilReady(2, TimeUnit.SECONDS);
+
+    assertTrue(unnamedA.connect(mqttUri(TOPIC_UNNAMED)));
+    assertTrue(singleA.connect(mqttUri(TOPIC_SINGLE)));
+    assertTrue(singleA.connect(mqttUri(TOPIC_SINGLE_ALTERNATIVE)));
+    assertTrue(optA.connect(mqttUri(TOPIC_OPT)));
+    assertTrue(listA1.connect(mqttUri(TOPIC_LIST_1)));
+    assertTrue(listA2.connect(mqttUri(TOPIC_LIST_2)));
+  }
+
+  @Override
+  protected void communicateSendInitialValue() throws IOException, InterruptedException {
+    // empty
+  }
+
+  @Override
+  protected void communicateOnlyUpdatedValue() throws IOException, InterruptedException {
+    check(INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE);
+
+    send(TOPIC_UNNAMED, "1");
+    check("1", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE);
+
+    send(TOPIC_SINGLE, "2");
+    check("1", "pre2", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE);
+
+    send(TOPIC_SINGLE, "2.1");
+    check("1", "pre2.1", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE);
+
+    send(TOPIC_SINGLE, "2.2");
+    check("1", "pre2.2", INITIAL_VALUE, INITIAL_VALUE, INITIAL_VALUE);
+
+    send(TOPIC_OPT, "3");
+    check("1", "pre2.2", "3post", INITIAL_VALUE, INITIAL_VALUE);
+
+    send(TOPIC_LIST_1, "4");
+    check("1", "pre2.2", "3post", "4", INITIAL_VALUE);
+
+    send(TOPIC_LIST_2, "5");
+    check("1", "pre2.2", "3post", "4", "5");
+
+    send(TOPIC_SINGLE_ALTERNATIVE, "fix");
+    check("1", "prefix", "3post", "4", "5");
+
+    assertTrue(model.getSingleA().disconnect(mqttUri(TOPIC_SINGLE)));
+    send(TOPIC_SINGLE, "6");
+    // no change to previous check since disconnected
+    check("1", "prefix", "3post", "4", "5");
+
+    send(TOPIC_SINGLE_ALTERNATIVE, "7");
+    // alternative topic is still active
+    check("1", "pre7", "3post", "4", "5");
+
+    assertTrue(model.getSingleA().disconnect(mqttUri(TOPIC_SINGLE_ALTERNATIVE)));
+    send(TOPIC_SINGLE_ALTERNATIVE, "8");
+    // no change to previous check since alternative topic is also disconnected now
+    check("1", "pre7", "3post", "4", "5");
+  }
+
+  private void send(String topic, String value) throws IOException, InterruptedException {
+    A a = new A().setValue(value);
+    try {
+      publisher.publish(topic, TestUtils.DefaultMappings.TreeToBytes(a::serialize));
+    } catch (SerializationException e) {
+      throw new IOException(e);
+    }
+    waitForMqtt();
+  }
+
+  @SuppressWarnings("StringEquality")
+  private void check(String unnamedValue, String singleValue, String optValue, String list1Value, String list2Value) {
+    if (INITIAL_VALUE == unnamedValue) {
+      assertEquals(unnamedA, model.getA());
+    } else {
+      assertNotEquals(unnamedA, model.getA());
+      assertEquals(unnamedValue, model.getA().getValue());
+    }
+
+    if (INITIAL_VALUE == singleValue) {
+      assertEquals(singleA, model.getSingleA());
+    } else {
+      assertNotEquals(singleA, model.getSingleA());
+      assertEquals(singleValue, model.getSingleA().getValue());
+    }
+
+    if (INITIAL_VALUE == optValue) {
+      assertEquals(optA, model.getOptA());
+    } else {
+      assertNotEquals(optA, model.getOptA());
+      assertEquals(optValue, model.getOptA().getValue());
+    }
+
+    if (INITIAL_VALUE == list1Value) {
+      assertEquals(listA1, model.getListA(0));
+    } else {
+      assertNotEquals(listA1, model.getListA(0));
+      assertEquals(list1Value, model.getListA(0).getValue());
+    }
+
+    if (INITIAL_VALUE == list2Value) {
+      assertEquals(listA2, model.getListA(1));
+    } else {
+      assertNotEquals(listA2, model.getListA(1));
+      assertEquals(list2Value, model.getListA(1).getValue());
+    }
+  }
+
+  @Override
+  protected void closeConnections() {
+
+  }
+}
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 bb557f6f88783c00f3bb2115758223bae0dc6f9c..984a5b06e2bb177ae13605c58fa9a95462dd74a8 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
@@ -1,10 +1,14 @@
 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.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.jastadd.ragconnect.compiler.Compiler;
 import org.junit.jupiter.api.Assertions;
 
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -14,6 +18,7 @@ import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 
 import static java.lang.Math.abs;
@@ -214,6 +219,10 @@ public class TestUtils {
         return defaultOnlyWrite.ast.ASTNode._apply__DefaultStringToBytesMapping(input);
       }
     }
+    @FunctionalInterface
+    interface SerializeFunction<E extends Throwable> {
+      void accept(JsonGenerator g, String fieldName) throws E;
+    }
 
     public static boolean BytesToBool(byte[] input) {
       try {
@@ -343,5 +352,13 @@ public class TestUtils {
         return null;
       }
     }
+    public static <E extends Throwable> byte[] TreeToBytes(SerializeFunction<E> serializeFunction) throws E, IOException {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      JsonFactory factory = new JsonFactory();
+      JsonGenerator generator = factory.createGenerator(outputStream, JsonEncoding.UTF8);
+      serializeFunction.accept(generator, null);
+      generator.flush();
+      return outputStream.toString().getBytes();
+    }
   }
 }