Data objects: add support for DO entity contributions

DO entity contributions allow to add payload in form of a specific DO
entity to an existing DO entity. The payload is part of an internal
attribute node _contributions. The contributions are designed in a way
that it is known which contribution are applicable to which DO entities
in order to determine a list of DO entities that can be held within
another DO entity.
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/DataObjectContributionTest.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/DataObjectContributionTest.java
new file mode 100644
index 0000000..e2f1a73
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/DataObjectContributionTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject;
+
+import static org.junit.Assert.*;
+
+import org.eclipse.scout.rt.dataobject.fixture.DoubleContributionFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.EntityFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.FirstSimpleContributionFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.ProjectContributionFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.ProjectFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.ScoutContributionFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.ScoutFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.SecondSimpleContributionFixtureDo;
+import org.eclipse.scout.rt.dataobject.fixture.SimpleFixtureDo;
+import org.eclipse.scout.rt.platform.BEANS;
+import org.eclipse.scout.rt.platform.util.Assertions.AssertionException;
+import org.eclipse.scout.rt.testing.platform.runner.PlatformTestRunner;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(PlatformTestRunner.class)
+public class DataObjectContributionTest {
+
+  @Test
+  public void testHasGetContribution() {
+    SimpleFixtureDo doEntity = BEANS.get(SimpleFixtureDo.class);
+    assertFalse(doEntity.has(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME)); // node doesn't exist
+    assertTrue(doEntity.getContributions().isEmpty());
+
+    assertThrows(AssertionException.class, () -> doEntity.getContribution(null)); // contribution class is mandatory
+
+    // has -> false, get -> null
+    assertFalse(doEntity.hasContribution(FirstSimpleContributionFixtureDo.class));
+    assertFalse(doEntity.hasContribution(SecondSimpleContributionFixtureDo.class));
+    assertNull(doEntity.getContribution(FirstSimpleContributionFixtureDo.class));
+    assertNull(doEntity.getContribution(SecondSimpleContributionFixtureDo.class));
+
+    // add first contribution
+    FirstSimpleContributionFixtureDo firstContribution = BEANS.get(FirstSimpleContributionFixtureDo.class);
+    doEntity.putContribution(firstContribution);
+
+    // check node availability and return values of has/get
+    assertEquals(1, doEntity.getContributions().size());
+    assertSame(firstContribution, doEntity.getContributions().iterator().next());
+    assertTrue(doEntity.has(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME));
+    assertTrue(doEntity.hasContribution(FirstSimpleContributionFixtureDo.class));
+    assertSame(firstContribution, doEntity.getContribution(FirstSimpleContributionFixtureDo.class));
+
+    // second contribution still not available
+    assertFalse(doEntity.hasContribution(SecondSimpleContributionFixtureDo.class));
+    assertNull(doEntity.getContribution(SecondSimpleContributionFixtureDo.class));
+  }
+
+  @Test
+  public void testContribution() {
+    SimpleFixtureDo doEntity = BEANS.get(SimpleFixtureDo.class);
+    assertNull(doEntity.getContribution(FirstSimpleContributionFixtureDo.class));
+    FirstSimpleContributionFixtureDo firstContribution = doEntity.contribution(FirstSimpleContributionFixtureDo.class);
+    assertNotNull(firstContribution);
+    assertSame(firstContribution, doEntity.contribution(FirstSimpleContributionFixtureDo.class)); // same instance if contribution is already available (via previous getOrCreate call)
+    assertSame(firstContribution, doEntity.getContribution(FirstSimpleContributionFixtureDo.class)); // same instance for get call
+
+    SecondSimpleContributionFixtureDo secondContribution = BEANS.get(SecondSimpleContributionFixtureDo.class);
+    doEntity.putContribution(secondContribution);
+    assertSame(secondContribution, doEntity.contribution(SecondSimpleContributionFixtureDo.class)); // same instance if contribution is already available (via putContribution)
+  }
+
+  @Test
+  public void testPutContribution() {
+    SimpleFixtureDo doEntity = BEANS.get(SimpleFixtureDo.class);
+    FirstSimpleContributionFixtureDo firstContribution1 = BEANS.get(FirstSimpleContributionFixtureDo.class);
+    doEntity.putContribution(firstContribution1);
+    assertEquals(1, doEntity.getContributions().size());
+    FirstSimpleContributionFixtureDo firstContribution2 = BEANS.get(FirstSimpleContributionFixtureDo.class);
+    doEntity.putContribution(firstContribution2);
+    assertEquals(1, doEntity.getContributions().size()); // size is still 1, first contribution was overridden
+
+    assertSame(firstContribution2, doEntity.getContribution(FirstSimpleContributionFixtureDo.class)); // same as 2. instance
+  }
+
+  @Test
+  public void testRemoveContribution() {
+    SimpleFixtureDo doEntity = BEANS.get(SimpleFixtureDo.class);
+    assertFalse(doEntity.removeContribution(FirstSimpleContributionFixtureDo.class)); // no effect
+    assertFalse(doEntity.removeContribution(SecondSimpleContributionFixtureDo.class)); // no effect
+    assertFalse(doEntity.has(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME)); // node doesn't exist
+
+    doEntity.putContribution(BEANS.get(FirstSimpleContributionFixtureDo.class));
+    assertTrue(doEntity.has(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME));
+    doEntity.putContribution(BEANS.get(SecondSimpleContributionFixtureDo.class));
+
+    assertTrue(doEntity.removeContribution(FirstSimpleContributionFixtureDo.class));
+    assertTrue(doEntity.has(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME));
+
+    assertTrue(doEntity.removeContribution(SecondSimpleContributionFixtureDo.class));
+    assertFalse(doEntity.has(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME)); // node is removed after last contribution is removed
+  }
+
+  @Test
+  public void testValidation() {
+    SimpleFixtureDo simpleFixture = BEANS.get(SimpleFixtureDo.class);
+    assertThrows(AssertionException.class, () -> simpleFixture.validateContributionClass(null)); // missing contribution class
+    simpleFixture.validateContributionClass(FirstSimpleContributionFixtureDo.class);
+    simpleFixture.validateContributionClass(SecondSimpleContributionFixtureDo.class);
+    assertThrows(AssertionException.class, () -> simpleFixture.validateContributionClass(ScoutContributionFixtureDo.class));
+    assertThrows(AssertionException.class, () -> simpleFixture.validateContributionClass(ProjectContributionFixtureDo.class));
+
+    // not using BEANS.get because bean is replaced by ProjectFixtureDo (only for validation, not a real case this way)
+    ScoutFixtureDo scoutFixture = new ScoutFixtureDo();
+    assertThrows(AssertionException.class, () -> scoutFixture.validateContributionClass(FirstSimpleContributionFixtureDo.class));
+    assertThrows(AssertionException.class, () -> scoutFixture.validateContributionClass(SecondSimpleContributionFixtureDo.class));
+    assertThrows(AssertionException.class, () -> scoutFixture.validateContributionClass(ProjectContributionFixtureDo.class));
+    scoutFixture.validateContributionClass(ScoutContributionFixtureDo.class);
+
+    // using subclasses data object
+    ProjectFixtureDo projectFixture = BEANS.get(ProjectFixtureDo.class);
+    assertThrows(AssertionException.class, () -> projectFixture.validateContributionClass(FirstSimpleContributionFixtureDo.class));
+    assertThrows(AssertionException.class, () -> projectFixture.validateContributionClass(SecondSimpleContributionFixtureDo.class));
+    projectFixture.validateContributionClass(ScoutContributionFixtureDo.class);
+    projectFixture.validateContributionClass(ProjectContributionFixtureDo.class);
+
+    // verify contribution DO with two containers
+    projectFixture.validateContributionClass(DoubleContributionFixtureDo.class);
+    simpleFixture.validateContributionClass(DoubleContributionFixtureDo.class);
+    assertThrows(AssertionException.class, () -> BEANS.get(EntityFixtureDo.class).validateContributionClass(SecondSimpleContributionFixtureDo.class));
+
+    // verify that all methods check validation
+    assertThrows(AssertionException.class, () -> simpleFixture.getContribution(ScoutContributionFixtureDo.class));
+    assertThrows(AssertionException.class, () -> simpleFixture.putContribution(BEANS.get(ScoutContributionFixtureDo.class)));
+    assertThrows(AssertionException.class, () -> simpleFixture.contribution(ScoutContributionFixtureDo.class));
+    assertThrows(AssertionException.class, () -> simpleFixture.hasContribution(ScoutContributionFixtureDo.class));
+    assertThrows(AssertionException.class, () -> simpleFixture.removeContribution(ScoutContributionFixtureDo.class));
+  }
+
+  @Test
+  public void testEquality() {
+    // Add contributions in order first -> second
+    SimpleFixtureDo doEntity1 = BEANS.get(SimpleFixtureDo.class);
+    doEntity1.putContribution(BEANS.get(FirstSimpleContributionFixtureDo.class));
+    doEntity1.putContribution(BEANS.get(SecondSimpleContributionFixtureDo.class));
+
+    // Add contributions in order second -> first
+    SimpleFixtureDo doEntity2 = BEANS.get(SimpleFixtureDo.class);
+    doEntity2.putContribution(BEANS.get(SecondSimpleContributionFixtureDo.class));
+    doEntity2.putContribution(BEANS.get(FirstSimpleContributionFixtureDo.class));
+
+    // Order of contributions must not be relevant for comparison
+    assertEquals(doEntity1, doEntity2);
+  }
+
+  @Test
+  public void testSerialization() {
+    IPrettyPrintDataObjectMapper mapper = BEANS.get(IPrettyPrintDataObjectMapper.class);
+
+    SimpleFixtureDo doEntity = BEANS.get(SimpleFixtureDo.class)
+        .withName1("name");
+
+    FirstSimpleContributionFixtureDo firstContribution = BEANS.get(FirstSimpleContributionFixtureDo.class).withFirstValue("first-value");
+    doEntity.putContribution(firstContribution);
+    SecondSimpleContributionFixtureDo secondContribution = BEANS.get(SecondSimpleContributionFixtureDo.class).withSecondValue("second-value");
+    doEntity.putContribution(secondContribution);
+
+    String json = mapper.writeValue(doEntity);
+
+    assertEquals("{\n"
+        + "  \"_type\" : \"scout.SimpleFixture\",\n"
+        + "  \"_contributions\" : [ {\n"
+        + "    \"_type\" : \"scout.FirstSimpleContributionFixture\",\n"
+        + "    \"firstValue\" : \"first-value\"\n"
+        + "  }, {\n"
+        + "    \"_type\" : \"scout.SecondSimpleContributionFixture\",\n"
+        + "    \"secondValue\" : \"second-value\"\n"
+        + "  } ],\n"
+        + "  \"name1\" : \"name\"\n"
+        + "}", json.replaceAll("\\r", ""));
+
+    SimpleFixtureDo deserializedDoEntity = mapper.readValue(json, SimpleFixtureDo.class);
+    assertEquals("name", deserializedDoEntity.getName1());
+    assertEquals(2, deserializedDoEntity.getContributions().size());
+    assertEquals(firstContribution, deserializedDoEntity.getContribution(FirstSimpleContributionFixtureDo.class));
+    assertEquals(secondContribution, deserializedDoEntity.getContribution(SecondSimpleContributionFixtureDo.class));
+    assertEquals(doEntity, deserializedDoEntity);
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/DoNodeTest.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/DoNodeTest.java
index f78c951..8d715cb 100644
--- a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/DoNodeTest.java
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/DoNodeTest.java
@@ -10,16 +10,12 @@
  */
 package org.eclipse.scout.rt.dataobject;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 
 import java.math.BigDecimal;
 import java.util.Optional;
 
-import org.eclipse.scout.rt.dataobject.DoNode;
+import org.eclipse.scout.rt.platform.holders.StringHolder;
 import org.junit.Test;
 
 /**
@@ -69,6 +65,23 @@
   }
 
   @Test
+  public void testIfPresent() {
+    FixtureDoNode<String> node = new FixtureDoNode<>();
+    assertFalse(node.exists());
+    node.ifPresent(value -> fail("node exists"));
+
+    node.create(); // node with null value
+    StringHolder holder = new StringHolder("other");
+    node.ifPresent(holder::setValue);
+    assertNull(holder.getValue()); // value was set
+
+    node.set("foo"); // node was created and contains a value
+    holder = new StringHolder("other");
+    node.ifPresent(holder::setValue);
+    assertEquals("foo", holder.getValue());
+  }
+
+  @Test
   public void testEqualsHashCode() {
     FixtureDoNode<String> node1 = new FixtureDoNode<>();
     FixtureDoNode<String> node2 = new FixtureDoNode<>();
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/DoubleContributionFixtureDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/DoubleContributionFixtureDo.java
new file mode 100644
index 0000000..a2dfe7f
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/DoubleContributionFixtureDo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.ContributesTo;
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.IDoEntityContribution;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("scout.DoubleContributionFixture")
+@ContributesTo({SimpleFixtureDo.class, ScoutFixtureDo.class})
+public final class DoubleContributionFixtureDo extends DoEntity implements IDoEntityContribution {
+
+  public DoValue<String> value() {
+    return doValue("value");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public DoubleContributionFixtureDo withValue(String value) {
+    value().set(value);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getValue() {
+    return value().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/FirstSimpleContributionFixtureDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/FirstSimpleContributionFixtureDo.java
new file mode 100644
index 0000000..e8c5233
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/FirstSimpleContributionFixtureDo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.ContributesTo;
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.IDoEntityContribution;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("scout.FirstSimpleContributionFixture")
+@ContributesTo(SimpleFixtureDo.class)
+public final class FirstSimpleContributionFixtureDo extends DoEntity implements IDoEntityContribution {
+
+  public DoValue<String> firstValue() {
+    return doValue("firstValue");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public FirstSimpleContributionFixtureDo withFirstValue(String firstValue) {
+    firstValue().set(firstValue);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getFirstValue() {
+    return firstValue().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/ProjectContributionFixtureDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/ProjectContributionFixtureDo.java
new file mode 100644
index 0000000..f15a1b5
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/ProjectContributionFixtureDo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.ContributesTo;
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.IDoEntityContribution;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("scout.ProjectContributionFixture")
+@ContributesTo(ProjectFixtureDo.class)
+public final class ProjectContributionFixtureDo extends DoEntity implements IDoEntityContribution {
+
+  public DoValue<String> value() {
+    return doValue("value");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public ProjectContributionFixtureDo withValue(String value) {
+    value().set(value);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getValue() {
+    return value().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/ScoutContributionFixtureDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/ScoutContributionFixtureDo.java
new file mode 100644
index 0000000..b3581e1
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/ScoutContributionFixtureDo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.ContributesTo;
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.IDoEntityContribution;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("scout.ScoutContributionFixture")
+@ContributesTo(ScoutFixtureDo.class)
+public final class ScoutContributionFixtureDo extends DoEntity implements IDoEntityContribution {
+
+  public DoValue<String> value() {
+    return doValue("value");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public ScoutContributionFixtureDo withValue(String value) {
+    value().set(value);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getValue() {
+    return value().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/SecondSimpleContributionFixtureDo.java b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/SecondSimpleContributionFixtureDo.java
new file mode 100644
index 0000000..4d94961
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject.test/src/test/java/org/eclipse/scout/rt/dataobject/fixture/SecondSimpleContributionFixtureDo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.ContributesTo;
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.IDoEntityContribution;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("scout.SecondSimpleContributionFixture")
+@ContributesTo(SimpleFixtureDo.class)
+public final class SecondSimpleContributionFixtureDo extends DoEntity implements IDoEntityContribution {
+
+  public DoValue<String> secondValue() {
+    return doValue("secondValue");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public SecondSimpleContributionFixtureDo withSecondValue(String secondValue) {
+    secondValue().set(secondValue);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getSecondValue() {
+    return secondValue().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/ContributesTo.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/ContributesTo.java
new file mode 100644
index 0000000..8f96db8
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/ContributesTo.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used for DO entity contributions
+ *
+ * @see IDoEntityContribution
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ContributesTo {
+
+  Class<? extends IDoEntity>[] value();
+}
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoEntity.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoEntity.java
index 1eac9cd..194a920 100644
--- a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoEntity.java
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoEntity.java
@@ -10,7 +10,7 @@
  */
 package org.eclipse.scout.rt.dataobject;
 
-import static org.eclipse.scout.rt.platform.util.Assertions.assertNotNull;
+import static org.eclipse.scout.rt.platform.util.Assertions.*;
 
 import java.util.Collection;
 import java.util.Collections;
@@ -22,6 +22,7 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 import org.eclipse.scout.rt.platform.BEANS;
 import org.eclipse.scout.rt.platform.util.Assertions;
@@ -50,6 +51,11 @@
  */
 public class DoEntity implements IDoEntity {
 
+  /**
+   * Attribute uses a {@link DoCollection} internally because order of elements is not relevant for equality.
+   */
+  public static final String CONTRIBUTIONS_ATTRIBUTE_NAME = "_contributions";
+
   private final Map<String, DoNode<?>> m_attributes = new LinkedHashMap<>();
 
   /**
@@ -196,6 +202,103 @@
   }
 
   @Override
+  public Collection<IDoEntityContribution> getContributions() {
+    if (!has(CONTRIBUTIONS_ATTRIBUTE_NAME)) {
+      return Collections.emptyList();
+    }
+
+    return Collections.unmodifiableCollection(getContributionsInternal());
+  }
+
+  @Override
+  public <CONTRIBUTION extends IDoEntityContribution> CONTRIBUTION getContribution(Class<CONTRIBUTION> contributionClass) {
+    validateContributionClass(contributionClass);
+    if (!has(CONTRIBUTIONS_ATTRIBUTE_NAME)) {
+      return null;
+    }
+
+    return getContributionsInternal().stream()
+        .filter(contribution -> contributionClass.equals(contribution.getClass()))
+        .findFirst()
+        .map(contributionClass::cast)
+        .orElse(null);
+  }
+
+  @Override
+  public <CONTRIBUTION extends IDoEntityContribution> void putContribution(CONTRIBUTION contribution) {
+    assertNotNull(contribution, "contribution is required");
+    validateContributionClass(contribution.getClass());
+
+    removeContribution(contribution.getClass());
+    ensureContributionsNode();
+    getContributionsInternal().add(contribution);
+  }
+
+  @Override
+  public <CONTRIBUTION extends IDoEntityContribution> CONTRIBUTION contribution(Class<CONTRIBUTION> contributionClass) {
+    validateContributionClass(contributionClass);
+
+    if (!hasContribution(contributionClass)) {
+      CONTRIBUTION contribution = BEANS.get(contributionClass);
+      putContribution(contribution);
+      return contribution;
+    }
+
+    return getContribution(contributionClass);
+  }
+
+  @Override
+  public boolean hasContribution(Class<? extends IDoEntityContribution> contributionClass) {
+    validateContributionClass(contributionClass);
+    return getContribution(contributionClass) != null;
+  }
+
+  @Override
+  public boolean removeContribution(Class<? extends IDoEntityContribution> contributionClass) {
+    validateContributionClass(contributionClass);
+    if (!has(CONTRIBUTIONS_ATTRIBUTE_NAME)) {
+      return false;
+    }
+
+    Collection<IDoEntityContribution> list = getContributionsInternal();
+    boolean removed = list.removeIf(contribution -> contributionClass.equals(contribution.getClass()));
+    if (list.isEmpty()) {
+      remove(CONTRIBUTIONS_ATTRIBUTE_NAME);
+    }
+    return removed;
+  }
+
+  protected void validateContributionClass(Class<? extends IDoEntityContribution> contributionClass) {
+    assertNotNull(contributionClass, "contributionClass is required");
+    ContributesTo contributesToAnn = contributionClass.getAnnotation(ContributesTo.class);
+    assertTrue(contributesToAnn != null && contributesToAnn.value() != null, "Contribution class {} is missing a valid {} annotation", contributionClass, ContributesTo.class.getSimpleName());
+    Class<? extends IDoEntity>[] containers = contributesToAnn.value();
+    assertTrue(Stream.of(containers).anyMatch(containerClass -> containerClass.isInstance(this)), "{} is not a valid container class of {}", this.getClass().getSimpleName(), contributionClass.getSimpleName());
+  }
+
+  /**
+   * Ensures that the contributions node ({@link #CONTRIBUTIONS_ATTRIBUTE_NAME}) exists (i.e. creates it if it doesn't
+   * exist yet).
+   */
+  protected void ensureContributionsNode() {
+    if (!has(CONTRIBUTIONS_ATTRIBUTE_NAME)) {
+      putNode(CONTRIBUTIONS_ATTRIBUTE_NAME, new DoCollection<IDoEntityContribution>());
+    }
+  }
+
+  /**
+   * Only call this method if the attribute node is available (e.g. call {@link #ensureContributionsNode()} before if
+   * desired or check for node existance manually).
+   *
+   * @return A mutable collection of DO entity contribution of corresponding {@link DoCollection} node.
+   */
+  protected Collection<IDoEntityContribution> getContributionsInternal() {
+    assertTrue(has(CONTRIBUTIONS_ATTRIBUTE_NAME), "Attribute node for DO entity contributions is missing");
+    //noinspection unchecked
+    return ((DoCollection<IDoEntityContribution>) Assertions.assertType(getNode(CONTRIBUTIONS_ATTRIBUTE_NAME), DoCollection.class)).get();
+  }
+
+  @Override
   public int hashCode() {
     final int prime = 31;
     int result = 1;
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoList.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoList.java
index 228e5ee..7646066 100644
--- a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoList.java
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoList.java
@@ -22,7 +22,7 @@
  * @see DoEntity#doList(String) creator method
  */
 @SuppressWarnings("squid:S2333") // redundant final
-public final class DoList<V> extends AbstractDoCollection<V, List<V>> implements IDataObject {
+public final class DoList<V> extends AbstractDoCollection<V, List<V>> {
 
   public DoList() {
     this(null, null);
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoNode.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoNode.java
index 50eba8e..cd76f72 100644
--- a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoNode.java
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/DoNode.java
@@ -78,6 +78,15 @@
   }
 
   /**
+   * Calls {@link Consumer#accept(Object)} if node exists.
+   */
+  public final void ifPresent(Consumer<T> consumer) {
+    if (exists()) {
+      consumer.accept(get());
+    }
+  }
+
+  /**
    * Internal method used to set attribute name when the node is added to a {@link DoEntity} object.
    */
   protected final void setAttributeName(String attributeName) {
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDataObject.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDataObject.java
index e03098c..e38081d 100644
--- a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDataObject.java
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDataObject.java
@@ -14,7 +14,8 @@
  * Marker interface for a data object.
  * <p>
  * Object-like data objects are represented using {@link IDoEntity} interface, {@link DoEntity} default implementation
- * and its subclasses, collection-like data objects are represented using {@link DoList}.
+ * and its subclasses, collection-like data objects are represented using {@link DoList}, {@link DoSet} or
+ * {@link DoCollection}.
  * <p>
  * Use this interface for deserialize any data object, whose content is not further specified, e.g:
  *
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoCollection.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoCollection.java
index b4a52a9..12d10f5 100644
--- a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoCollection.java
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoCollection.java
@@ -20,7 +20,7 @@
 /**
  * Interface for a generic collection of values of type {@code V} inside a {@link DoEntity} object.
  */
-public interface IDoCollection<V, COLLECTION extends Collection<V>> extends Iterable<V> {
+public interface IDoCollection<V, COLLECTION extends Collection<V>> extends IDataObject, Iterable<V> {
 
   /**
    * @return {@code true} if this attribute is part of a {@link DoEntity}, otherwise {@code false}.
@@ -30,7 +30,7 @@
   /**
    * @return modifiable collection of all items, never {@code null}.
    */
-  public COLLECTION get();
+  COLLECTION get();
 
   /**
    * Returns <code>true</code> if this collection contains the item, <code>false</code> otherwise.
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoEntity.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoEntity.java
index a58a961..64975f1 100644
--- a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoEntity.java
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoEntity.java
@@ -323,4 +323,48 @@
   default boolean isEmpty() {
     return allNodes().isEmpty();
   }
+
+  /**
+   * Doesn't create the internal contributions node if there is none yet.
+   *
+   * @return An immutable collection of DO entity contributions or an empty list if there a none.
+   */
+  Collection<IDoEntityContribution> getContributions();
+
+  /**
+   * Doesn't create the internal contributions node if there is none yet.
+   *
+   * @return DO entity contribution for this contribution class if available, <code>null</code> otherwise.
+   */
+  <CONTRIBUTION extends IDoEntityContribution> CONTRIBUTION getContribution(Class<CONTRIBUTION> contributionClass);
+
+  /**
+   * Adds a new DO entity contribution. An existing contribution for the same contribution class is overridden.
+   *
+   * @param contribution
+   *          Contribution to add.
+   */
+  <CONTRIBUTION extends IDoEntityContribution> void putContribution(CONTRIBUTION contribution);
+
+  /**
+   * @return Existing DO entity contribution for this contribution class if available, otherwise creates a new DO entity
+   *         contribution instance, adds it to the contributions and returns it.
+   */
+  <CONTRIBUTION extends IDoEntityContribution> CONTRIBUTION contribution(Class<CONTRIBUTION> contributionClass);
+
+  /**
+   * Doesn't create the internal contributions node if there is none yet.
+   *
+   * @return <code>true</code> if the DO entity contribution for this contribution class is available,
+   *         <code>false</code> otherwise.
+   */
+  boolean hasContribution(Class<? extends IDoEntityContribution> contributionClass);
+
+  /**
+   * The internal contribution node is removed if there are no remaining contributions after removal of the given
+   * contribution.
+   *
+   * @return <code>true</code> if the DO entity contribution was available and removed, <code>false</code> otherwise.
+   */
+  boolean removeContribution(Class<? extends IDoEntityContribution> contributionClass);
 }
diff --git a/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoEntityContribution.java b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoEntityContribution.java
new file mode 100644
index 0000000..9a29b5d
--- /dev/null
+++ b/org.eclipse.scout.rt.dataobject/src/main/java/org/eclipse/scout/rt/dataobject/IDoEntityContribution.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2010-2021 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.dataobject;
+
+/**
+ * Interface for DO entity contributions.
+ * <ul>
+ * <li>Each implementation must be final (no subclassing of contributions).
+ * <li>Each implementation must have a {@link ContributesTo} annotation with a non-empty {@link ContributesTo#value()}
+ * attribute.
+ * </ul>
+ */
+public interface IDoEntityContribution extends IDoEntity {
+}
diff --git a/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsRawSerializationTest.java b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsRawSerializationTest.java
index 65ad5a4..f9f17f5 100644
--- a/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsRawSerializationTest.java
+++ b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsRawSerializationTest.java
@@ -73,6 +73,12 @@
   }
 
   @Test
+  public void testDoEntityWithContributions() {
+    DoEntity doEntity = (DoEntity) testRawDataObjectMapper("TestDoEntityWithContributions.json");
+    assertTrue(doEntity.getNode(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME) instanceof DoCollection); // even when using raw deserialization, contributions node is always DoCollection
+  }
+
+  @Test
   public void testVersionedDo() {
     TestVersionedDo versioned = BEANS.get(TestVersionedDo.class).withName("lorem");
     String json = s_dataObjectMapper.writeValue(versioned);
@@ -106,10 +112,11 @@
     assertEquals("str2", ((DoEntity) item).get("stringAttribute"));
   }
 
-  protected void testRawDataObjectMapper(String jsonFileName) {
+  protected IDataObject testRawDataObjectMapper(String jsonFileName) {
     String json = readResourceAsString(jsonFileName);
     IDataObject object = s_dataObjectMapper.readValueRaw(json);
     assertNoTypes(object);
+    return object;
   }
 
   /**
diff --git a/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsSerializationTest.java b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsSerializationTest.java
index 19ef06a..35980a0 100644
--- a/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsSerializationTest.java
+++ b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/JsonDataObjectsSerializationTest.java
@@ -48,6 +48,7 @@
 import org.eclipse.scout.rt.dataobject.IDoEntity;
 import org.eclipse.scout.rt.dataobject.IValueFormatConstants;
 import org.eclipse.scout.rt.jackson.dataobject.fixture.ITestBaseEntityDo;
+import org.eclipse.scout.rt.jackson.dataobject.fixture.OneTestItemContributionDo;
 import org.eclipse.scout.rt.jackson.dataobject.fixture.TestBigIntegerDo;
 import org.eclipse.scout.rt.jackson.dataobject.fixture.TestBinaryDo;
 import org.eclipse.scout.rt.jackson.dataobject.fixture.TestBinaryResourceDo;
@@ -2090,6 +2091,19 @@
     assertEquals("foo-id-1", marshalled.genericMapAttribute().get().get("foo-1").getId());
   }
 
+  @Test
+  public void testSerializeDeserialize_DoEntityWithContributions() throws Exception {
+    TestItemDo doEntity = BEANS.get(TestItemDo.class).withId("123456789");
+    doEntity.contribution(OneTestItemContributionDo.class).withName("my");
+
+    String json = s_dataObjectMapper.writeValueAsString(doEntity);
+    assertJsonEquals("TestDoEntityWithContributions.json", json);
+
+    TestItemDo marshalledDoEntity = s_dataObjectMapper.readValue(json, TestItemDo.class);
+    assertEqualsWithComparisonFailure(doEntity, marshalledDoEntity);
+    assertTrue(doEntity.getNode(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME) instanceof DoCollection); // contributions node is always DoCollection
+  }
+
   // ------------------------------------ entity with IDoEntity interface definition tests -----------------------------
 
   @Test
diff --git a/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/fixture/OneTestItemContributionDo.java b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/fixture/OneTestItemContributionDo.java
new file mode 100644
index 0000000..f806daf
--- /dev/null
+++ b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/fixture/OneTestItemContributionDo.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2010-2018 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ */
+package org.eclipse.scout.rt.jackson.dataobject.fixture;
+
+import javax.annotation.Generated;
+
+import org.eclipse.scout.rt.dataobject.ContributesTo;
+import org.eclipse.scout.rt.dataobject.DoEntity;
+import org.eclipse.scout.rt.dataobject.DoValue;
+import org.eclipse.scout.rt.dataobject.IDoEntityContribution;
+import org.eclipse.scout.rt.dataobject.TypeName;
+
+@TypeName("OneTestItemContribution")
+@ContributesTo(TestItemDo.class)
+public class OneTestItemContributionDo extends DoEntity implements IDoEntityContribution {
+
+  public DoValue<String> name() {
+    return doValue("name");
+  }
+
+  /* **************************************************************************
+   * GENERATED CONVENIENCE METHODS
+   * *************************************************************************/
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public OneTestItemContributionDo withName(String name) {
+    name().set(name);
+    return this;
+  }
+
+  @Generated("DoConvenienceMethodsGenerator")
+  public String getName() {
+    return name().get();
+  }
+}
diff --git a/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/fixture/TestCustomImplementedEntityDo.java b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/fixture/TestCustomImplementedEntityDo.java
index eff8359..5e14a5f 100644
--- a/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/fixture/TestCustomImplementedEntityDo.java
+++ b/org.eclipse.scout.rt.jackson.test/src/test/java/org/eclipse/scout/rt/jackson/dataobject/fixture/TestCustomImplementedEntityDo.java
@@ -30,6 +30,7 @@
 import org.eclipse.scout.rt.dataobject.DoSet;
 import org.eclipse.scout.rt.dataobject.DoValue;
 import org.eclipse.scout.rt.dataobject.IDoEntity;
+import org.eclipse.scout.rt.dataobject.IDoEntityContribution;
 import org.eclipse.scout.rt.dataobject.IValueFormatConstants;
 import org.eclipse.scout.rt.dataobject.TypeName;
 import org.eclipse.scout.rt.dataobject.ValueFormat;
@@ -120,6 +121,36 @@
   }
 
   @Override
+  public Collection<IDoEntityContribution> getContributions() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <CONTRIBUTION extends IDoEntityContribution> CONTRIBUTION getContribution(Class<CONTRIBUTION> contributionClass) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <CONTRIBUTION extends IDoEntityContribution> void putContribution(CONTRIBUTION contribution) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <CONTRIBUTION extends IDoEntityContribution> CONTRIBUTION contribution(Class<CONTRIBUTION> contributionClass) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean hasContribution(Class<? extends IDoEntityContribution> contributionClass) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean removeContribution(Class<? extends IDoEntityContribution> contributionClass) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public boolean equals(Object obj) {
     return ObjectUtility.equals(m_attributes, ((TestCustomImplementedEntityDo) obj).m_attributes);
   }
diff --git a/org.eclipse.scout.rt.jackson.test/src/test/resources/org/eclipse/scout/rt/jackson/dataobject/TestDoEntityWithContributions.json b/org.eclipse.scout.rt.jackson.test/src/test/resources/org/eclipse/scout/rt/jackson/dataobject/TestDoEntityWithContributions.json
new file mode 100644
index 0000000..b04b779
--- /dev/null
+++ b/org.eclipse.scout.rt.jackson.test/src/test/resources/org/eclipse/scout/rt/jackson/dataobject/TestDoEntityWithContributions.json
@@ -0,0 +1,8 @@
+{
+  "_type" : "TestItem",
+  "_contributions" : [ {
+    "_type" : "OneTestItemContribution",
+    "name" : "my"
+  } ],
+  "id" : "123456789"
+}
diff --git a/org.eclipse.scout.rt.jackson/src/main/java/org/eclipse/scout/rt/jackson/dataobject/DoEntityDeserializer.java b/org.eclipse.scout.rt.jackson/src/main/java/org/eclipse/scout/rt/jackson/dataobject/DoEntityDeserializer.java
index 578e194..01daefc 100644
--- a/org.eclipse.scout.rt.jackson/src/main/java/org/eclipse/scout/rt/jackson/dataobject/DoEntityDeserializer.java
+++ b/org.eclipse.scout.rt.jackson/src/main/java/org/eclipse/scout/rt/jackson/dataobject/DoEntityDeserializer.java
@@ -195,10 +195,10 @@
 
   protected JavaType findResolvedAttributeType(IDoEntity entityInstance, String attributeName, boolean isObject, boolean isArray) {
     return m_doEntityDeserializerTypeResolver.resolveAttributeType(entityInstance.getClass(), attributeName)
-        .orElseGet(() -> findResolvedFallbackAttributeType(isObject, isArray));
+        .orElseGet(() -> findResolvedFallbackAttributeType(attributeName, isObject, isArray));
   }
 
-  protected JavaType findResolvedFallbackAttributeType(boolean isObject, boolean isArray) {
+  protected JavaType findResolvedFallbackAttributeType(String attributeName, boolean isObject, boolean isArray) {
     if (DoMapEntity.class.isAssignableFrom(m_handledClass)) {
       // DoMapEntity<T> structure is deserialized as typed Map<String, T>
       return findResolvedDoMapEntityType();
@@ -209,7 +209,8 @@
     }
     else if (isArray) {
       // array-like JSON structure is deserialized as raw DoList (using DoList as generic structure instead of DoSet or DoCollection)
-      return TypeFactory.defaultInstance().constructType(DoList.class);
+      // Exception: internal node _contributions is deserialized using a DoCollection
+      return TypeFactory.defaultInstance().constructType(DoEntity.CONTRIBUTIONS_ATTRIBUTE_NAME.equals(attributeName) ? DoCollection.class : DoList.class);
     }
     else {
       // JSON scalar values are deserialized as raw object using default jackson typing