Repository: CycloneDX/cyclonedx-core-java
Pull request #76 (2021-02-18 20:54:26)

Allow for wrappers with localName of component, but different wrapper name
This is specifically aimed at addressing #75 , I tried to come up with a minimally obtrusive way of doing this (Jackson no likey wrapper with same local name, thus the infamous wrapper class), by introducing a abstract `ComponentWrapper` class that can be used to quickly implement this (and allow for growth of the other classes moving forward.

Tested for both XML, JSON parsing and generation, everything seems to be passing, flying colors.

We have also fixed a few small bugs we found along the way while battle testing:
- Make sure single CWEs get added to a vulnerability, fixed in: e06a5ac
- Process source for vulnerability: a2fe9c4 
- Fix missing endElement: 1a4c5fd


Patch:
diff --git a/src/main/java/org/cyclonedx/BomGeneratorFactory.java b/src/main/java/org/cyclonedx/BomGeneratorFactory.java
index afc14aae6..0667ed9a8 100644
--- a/src/main/java/org/cyclonedx/BomGeneratorFactory.java
+++ b/src/main/java/org/cyclonedx/BomGeneratorFactory.java
@@ -37,16 +37,20 @@ public class BomGeneratorFactory {
     }
 
     public static BomXmlGenerator createXml(CycloneDxSchema.Version version, Bom bom) {
-        if (CycloneDxSchema.Version.VERSION_10 == version) {
-            return new BomXmlGenerator10(bom);
-        } else if (CycloneDxSchema.Version.VERSION_11 == version) {
-            return new BomXmlGenerator11(bom);
-        } else {
-            return new BomXmlGenerator12(bom);
+        switch (version) {
+            case VERSION_10:
+                return new BomXmlGenerator10(bom);
+            case VERSION_11:
+                return new BomXmlGenerator11(bom);
+            default:
+                return new BomXmlGenerator12(bom);
         }
     }
 
-    public static BomJsonGenerator createJson(CycloneDxSchema.Version version, Bom bom) {
-        return new BomJsonGenerator12(bom);
+    public static BomJsonGenerator createJson(final CycloneDxSchema.Version version,  Bom bom) {
+        switch (version) {
+            default:
+                return new BomJsonGenerator12(bom);
+        }
     }
 }
diff --git a/src/main/java/org/cyclonedx/generators/json/AbstractBomJsonGenerator.java b/src/main/java/org/cyclonedx/generators/json/AbstractBomJsonGenerator.java
index 5e0d405a8..82fd13da7 100644
--- a/src/main/java/org/cyclonedx/generators/json/AbstractBomJsonGenerator.java
+++ b/src/main/java/org/cyclonedx/generators/json/AbstractBomJsonGenerator.java
@@ -25,6 +25,7 @@ import org.cyclonedx.CycloneDxSchema;
 import org.cyclonedx.exception.GeneratorException;
 import org.cyclonedx.model.Bom;
 import org.cyclonedx.util.CollectionTypeSerializer;
+import org.cyclonedx.util.ComponentWrapperSerializer;
 import org.cyclonedx.util.LicenseChoiceSerializer;
 import org.json.JSONObject;
 
@@ -41,6 +42,7 @@ abstract class AbstractBomJsonGenerator extends CycloneDxSchema implements BomJs
     private void setupObjectMapper(final ObjectMapper mapper) {
         SimpleModule licenseModule = new SimpleModule();
         SimpleModule depModule = new SimpleModule();
+        SimpleModule componentWrapperModule = new SimpleModule();
 
         licenseModule.addSerializer(new LicenseChoiceSerializer());
 
@@ -48,6 +50,10 @@ abstract class AbstractBomJsonGenerator extends CycloneDxSchema implements BomJs
 
         depModule.setSerializers(new CollectionTypeSerializer(false));
         mapper.registerModule(depModule);
+
+        componentWrapperModule.addSerializer(new ComponentWrapperSerializer(mapper));
+
+        mapper.registerModule(componentWrapperModule);
     }
 
     JSONObject doc = OrderedJSONObjectFactory.create();
diff --git a/src/main/java/org/cyclonedx/model/Ancestors.java b/src/main/java/org/cyclonedx/model/Ancestors.java
new file mode 100644
index 000000000..442255abe
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/Ancestors.java
@@ -0,0 +1,22 @@
+/*
+ * This file is part of CycloneDX Core (Java).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+public class Ancestors extends ComponentWrapper {
+}
diff --git a/src/main/java/org/cyclonedx/model/ComponentWrapper.java b/src/main/java/org/cyclonedx/model/ComponentWrapper.java
new file mode 100644
index 000000000..f633fb3a3
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/ComponentWrapper.java
@@ -0,0 +1,53 @@
+/*
+ * This file is part of CycloneDX Core (Java).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+/**
+ * Helper class for Jackson serializing/deserializing lists that have same localname, but different wrapper name.
+ * Currently used by Ancestors, Descendants and Variants
+ * Workaround for: https://github.com/FasterXML/jackson-dataformat-xml/issues/192
+ * @since 4.0.0
+ */
+public abstract class ComponentWrapper
+{
+  protected List<Component> components;
+
+  @JacksonXmlElementWrapper(useWrapping = false)
+  @JacksonXmlProperty(localName = "component")
+  public List<Component> getComponents() {
+    return components;
+  }
+
+  public void setComponents(final List<Component> components) {
+    this.components = components;
+  }
+
+  public void addComponent(final Component component) {
+    if (this.components == null) {
+      this.components = new ArrayList<>();
+    }
+    this.components.add(component);
+  }
+}
diff --git a/src/main/java/org/cyclonedx/model/Descendants.java b/src/main/java/org/cyclonedx/model/Descendants.java
new file mode 100644
index 000000000..2917c443f
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/Descendants.java
@@ -0,0 +1,22 @@
+/*
+ * This file is part of CycloneDX Core (Java).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+public class Descendants extends ComponentWrapper {
+}
diff --git a/src/main/java/org/cyclonedx/model/Pedigree.java b/src/main/java/org/cyclonedx/model/Pedigree.java
index e5c2c0b36..b50223da5 100644
--- a/src/main/java/org/cyclonedx/model/Pedigree.java
+++ b/src/main/java/org/cyclonedx/model/Pedigree.java
@@ -20,43 +20,46 @@ package org.cyclonedx.model;
 
 import java.util.List;
 import java.util.Objects;
+
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import org.cyclonedx.util.ComponentWrapperDeserializer;
 
 @SuppressWarnings("unused")
-@JsonPropertyOrder({"ancestors", "commits", "notes"})
+@JsonPropertyOrder({"ancestors", "descendants", "variants", "commits", "notes"})
 public class Pedigree extends ExtensibleElement {
 
-    private List<Component> ancestors;
-    private List<Component> descendants;
-    private List<Component> variants;
+    @JsonDeserialize(using = ComponentWrapperDeserializer.class)
+    private Ancestors ancestors;
+
+    @JsonDeserialize(using = ComponentWrapperDeserializer.class)
+    private Descendants descendants;
+
+    @JsonDeserialize(using = ComponentWrapperDeserializer.class)
+    private Variants variants;
     private List<Commit> commits;
     private String notes;
 
-    @JacksonXmlElementWrapper(localName = "ancestors")
-    @JacksonXmlProperty(localName = "component")
-    public List<Component> getAncestors() {
+    public Ancestors getAncestors() {
         return ancestors;
     }
-
-    public void setAncestors(List<Component> ancestors) {
+    public void setAncestors(final Ancestors ancestors) {
         this.ancestors = ancestors;
     }
 
-    public List<Component> getDescendants() {
+    public Descendants getDescendants() {
         return descendants;
     }
-
-    public void setDescendants(List<Component> descendants) {
+    public void setDescendants(final Descendants descendants) {
         this.descendants = descendants;
     }
 
-    public List<Component> getVariants() {
+    public Variants getVariants() {
         return variants;
     }
-
-    public void setVariants(List<Component> variants) {
+    public void setVariants(final Variants variants) {
         this.variants = variants;
     }
 
@@ -84,10 +87,10 @@ public class Pedigree extends ExtensibleElement {
         if (!(o instanceof Pedigree)) return false;
         Pedigree pedigree = (Pedigree) o;
         return Objects.equals(ancestors, pedigree.ancestors) &&
-                Objects.equals(descendants, pedigree.descendants) &&
-                Objects.equals(variants, pedigree.variants) &&
-                Objects.equals(commits, pedigree.commits) &&
-                Objects.equals(notes, pedigree.notes);
+            Objects.equals(descendants, pedigree.descendants) &&
+            Objects.equals(variants, pedigree.variants) &&
+            Objects.equals(commits, pedigree.commits) &&
+            Objects.equals(notes, pedigree.notes);
     }
 
     @Override
diff --git a/src/main/java/org/cyclonedx/model/Variants.java b/src/main/java/org/cyclonedx/model/Variants.java
new file mode 100644
index 000000000..57ccd6f48
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/Variants.java
@@ -0,0 +1,22 @@
+/*
+ * This file is part of CycloneDX Core (Java).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+public class Variants extends ComponentWrapper {
+}
diff --git a/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java b/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java
index da67321e0..036b4e73e 100644
--- a/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java
+++ b/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java
@@ -37,6 +37,7 @@ public class Vulnerability10
   public static final String NAME = "vulnerability";
   public static final String ID = "id";
   public static final String SOURCE = "source";
+  public static final String SOURCE_NAME = "name";
   public static final String URL = "url";
   public static final String VULNERABILITIES = "vulnerabilities";
   public static final String RATINGS = "ratings";
diff --git a/src/main/java/org/cyclonedx/util/ComponentWrapperDeserializer.java b/src/main/java/org/cyclonedx/util/ComponentWrapperDeserializer.java
new file mode 100644
index 000000000..7073071bc
--- /dev/null
+++ b/src/main/java/org/cyclonedx/util/ComponentWrapperDeserializer.java
@@ -0,0 +1,81 @@
+/*
+ * This file is part of CycloneDX Core (Java).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.util;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser;
+import org.cyclonedx.model.Ancestors;
+import org.cyclonedx.model.Component;
+import org.cyclonedx.model.ComponentWrapper;
+import org.cyclonedx.model.Descendants;
+import org.cyclonedx.model.Variants;
+
+public class ComponentWrapperDeserializer extends JsonDeserializer<ComponentWrapper>
+{
+  private static final String ANCESTORS = "ancestors";
+  private static final String DESCENDANTS = "descendants";
+  private static final String VARIANTS = "variants";
+
+  @Override
+  public ComponentWrapper deserialize(
+      final JsonParser parser, final DeserializationContext context)
+      throws IOException
+  {
+    final String location = parser.getCurrentName();
+    if (parser instanceof FromXmlParser) {
+      switch (location) {
+        case ANCESTORS:
+          return parser.readValueAs(Ancestors.class);
+        case DESCENDANTS:
+          return parser.readValueAs(Descendants.class);
+        case VARIANTS:
+          return parser.readValueAs(Variants.class);
+        default:
+          return null;
+      }
+    }
+
+    ComponentWrapper wrapper;
+
+    switch (location) {
+      case ANCESTORS:
+        wrapper = new Ancestors();
+        break;
+      case DESCENDANTS:
+        wrapper = new Descendants();
+        break;
+      case VARIANTS:
+        wrapper = new Variants();
+        break;
+      default:
+        return null;
+    }
+
+    Component[] components = parser.readValueAs(Component[].class);
+
+    wrapper.setComponents(Arrays.asList(components));
+
+    return wrapper;
+  }
+}
diff --git a/src/main/java/org/cyclonedx/util/ComponentWrapperSerializer.java b/src/main/java/org/cyclonedx/util/ComponentWrapperSerializer.java
new file mode 100644
index 000000000..98a5b046c
--- /dev/null
+++ b/src/main/java/org/cyclonedx/util/ComponentWrapperSerializer.java
@@ -0,0 +1,56 @@
+/*
+ * This file is part of CycloneDX Core (Java).
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * Copyright (c) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.util;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import org.cyclonedx.model.ComponentWrapper;
+
+public class ComponentWrapperSerializer extends StdSerializer<ComponentWrapper>
+{
+  private ObjectMapper mapper;
+
+  public ComponentWrapperSerializer(final ObjectMapper mapper) {
+    this(ComponentWrapper.class);
+
+    this.mapper = mapper;
+  }
+
+  public ComponentWrapperSerializer(final Class<ComponentWrapper> t) {
+    super(t);
+
+    mapper = new ObjectMapper();
+  }
+
+  @Override
+  public void serialize(
+      final ComponentWrapper componentWrapper,
+      final JsonGenerator generator,
+      final SerializerProvider serializerProvider)
+      throws IOException
+  {
+    if (componentWrapper.getComponents() != null) {
+      mapper.writeValue(generator, componentWrapper.getComponents());
+    }
+  }
+}
diff --git a/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java b/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java
index fb21c4161..5abe605ae 100644
--- a/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java
+++ b/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java
@@ -168,7 +168,7 @@ public class ExtensionDeserializer extends StdDeserializer<Extension>
         cweList.add(processCwe(c));
       }
     } else {
-      processCwe(cwe);
+      cweList.add(processCwe(cwe));
     }
     return cweList.isEmpty() ? null : cweList;
   }
diff --git a/src/main/java/org/cyclonedx/util/ExtensionSerializer.java b/src/main/java/org/cyclonedx/util/ExtensionSerializer.java
index 0a432e79f..ee92e66c3 100644
--- a/src/main/java/org/cyclonedx/util/ExtensionSerializer.java
+++ b/src/main/java/org/cyclonedx/util/ExtensionSerializer.java
@@ -43,7 +43,7 @@ public class ExtensionSerializer
     this(null);
   }
 
-  public ExtensionSerializer(final Class t) {
+  public ExtensionSerializer(final Class<Extension> t) {
     super(t);
   }
 
@@ -80,6 +80,7 @@ public class ExtensionSerializer
       generateTextNode(staxWriter, Vulnerability10.DESCRIPTION, vuln.getDescription(), Vulnerability10.NAMESPACE_URI, Vulnerability10.PREFIX);
       processRecommendations(staxWriter, vuln);
       processAdvisories(staxWriter, vuln);
+      processSource(staxWriter, vuln);
 
       staxWriter.writeEndElement();
     }
@@ -123,6 +124,20 @@ public class ExtensionSerializer
     }
   }
 
+  private void processSource(final XMLStreamWriter staxWriter, final Vulnerability10 vulnerability)
+      throws XMLStreamException
+  {
+    if (vulnerability.getSource() != null && vulnerability.getSource().getName() != null) {
+      staxWriter.writeStartElement(Vulnerability10.PREFIX, Vulnerability10.SOURCE, Vulnerability10.NAMESPACE_URI);
+      staxWriter.writeAttribute(Vulnerability10.SOURCE_NAME, vulnerability.getSource().getName());
+      if (vulnerability.getSource().getUrl() != null) {
+        generateTextNode(staxWriter, Vulnerability10.URL, vulnerability.getSource().getUrl().toString(),
+            Vulnerability10.NAMESPACE_URI, Vulnerability10.PREFIX);
+      }
+      staxWriter.writeEndElement();
+    }
+  }
+
   private void processRatings(final XMLStreamWriter staxWriter, final Vulnerability10 vuln)
       throws XMLStreamException
   {


Test patch:
diff --git a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
index c9b6262fe..7d0ab322a 100644
--- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
+++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java
@@ -1,6 +1,7 @@
 package org.cyclonedx;
 
 import org.apache.commons.io.IOUtils;
+import org.cyclonedx.CycloneDxSchema.Version;
 import org.cyclonedx.generators.json.BomJsonGenerator;
 import org.cyclonedx.generators.json.BomJsonGenerator12;
 import org.cyclonedx.model.Bom;
@@ -35,7 +36,7 @@ public class BomJsonGeneratorTest {
     @Test
     public void schema12GenerationTest() throws Exception {
         Bom bom =  createCommonBom("/bom-1.2.xml");
-        BomJsonGenerator generator = BomGeneratorFactory.createJson(CycloneDxSchema.Version.VERSION_12, bom);
+        BomJsonGenerator generator = BomGeneratorFactory.createJson(Version.VERSION_12, bom);
         generator.generate();
         Assert.assertTrue(generator instanceof BomJsonGenerator12);
         Assert.assertEquals(CycloneDxSchema.Version.VERSION_12, generator.getSchemaVersion());
diff --git a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java
index a6e662d7d..164ffbfdb 100644
--- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java
+++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java
@@ -147,6 +147,19 @@ public class BomXmlGeneratorTest {
         Assert.assertTrue(parser.isValid(file, CycloneDxSchema.Version.VERSION_12));
     }
 
+    @Test
+    public void schema12GenerationWithPedigreeDataTest() throws Exception {
+        BomXmlGenerator generator = BomGeneratorFactory.createXml(CycloneDxSchema.Version.VERSION_12, createCommonBom("/bom-1.2-pedigree.xml"));
+        Document doc = generator.generate();
+        testDocument(doc);
+
+        Assert.assertTrue(generator instanceof BomXmlGenerator12);
+        Assert.assertEquals(CycloneDxSchema.Version.VERSION_12, generator.getSchemaVersion());
+        File file = writeToFile(generator.toXmlString());
+        XmlParser parser = new XmlParser();
+        Assert.assertTrue(parser.isValid(file, CycloneDxSchema.Version.VERSION_12));
+    }
+
     @Test
     public void invalidUrlTest() throws Exception {
         Component c = new Component();
diff --git a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
index c0769d898..8ea3c1a4a 100644
--- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java
@@ -85,6 +85,9 @@ public class JsonParserTest {
         Assert.assertEquals("9.0.14", c1.getVersion());
         Assert.assertEquals(Component.Type.LIBRARY, c1.getType());
         Assert.assertEquals("pkg:npm/acme/component@1.0.0", c1.getPurl());
+
+        testPedigree(c1);
+
         // Begin Services
         List<Service> services = bom.getServices();
         Assert.assertEquals(1, services.size());
@@ -130,6 +133,7 @@ public class JsonParserTest {
         Assert.assertEquals(ExternalReference.Type.DOCUMENTATION, s.getExternalReferences().get(1).getType());
         Assert.assertEquals("http://api.partner.org/swagger", s.getExternalReferences().get(1).getUrl());
         // End Services
+
         // Begin Dependencies
         Assert.assertEquals(1, bom.getDependencies().size());
         Dependency d1 = bom.getDependencies().get(0);
@@ -140,4 +144,14 @@ public class JsonParserTest {
         Assert.assertEquals("pkg:npm/acme/common@1.0.0", d11.getRef());
         Assert.assertNull(d11.getDependencies());
     }
+
+    private void testPedigree(final Component component) {
+        Assert.assertNotNull(component.getPedigree());
+        Assert.assertNotNull(component.getPedigree().getAncestors());
+        Assert.assertEquals(2, component.getPedigree().getAncestors().getComponents().size());
+        Assert.assertNotNull(component.getPedigree().getDescendants());
+        Assert.assertEquals(2, component.getPedigree().getDescendants().getComponents().size());
+        Assert.assertNotNull(component.getPedigree().getVariants());
+        Assert.assertEquals(2, component.getPedigree().getVariants().getComponents().size());
+    }
 }
diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
index 17b4b40fa..326cfcc1a 100644
--- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
@@ -20,7 +20,15 @@ package org.cyclonedx.parsers;
 
 import org.apache.commons.io.IOUtils;
 import org.cyclonedx.CycloneDxSchema;
-import org.cyclonedx.model.*;
+import org.cyclonedx.model.Bom;
+import org.cyclonedx.model.Component;
+import org.cyclonedx.model.Dependency;
+import org.cyclonedx.model.ExternalReference;
+import org.cyclonedx.model.OrganizationalContact;
+import org.cyclonedx.model.OrganizationalEntity;
+import org.cyclonedx.model.Pedigree;
+import org.cyclonedx.model.Service;
+import org.cyclonedx.model.ServiceData;
 import org.junit.Assert;
 import org.junit.Test;
 import java.io.File;
@@ -69,6 +77,23 @@ public class XmlParserTest {
         Assert.assertTrue(valid);
     }
 
+    @Test
+    public void testValid12BomWithPedigree() throws Exception {
+        final File file = new File(this.getClass().getResource("/bom-1.2-pedigree.xml").getFile());
+        final XmlParser parser = new XmlParser();
+        final boolean valid = parser.isValid(file, CycloneDxSchema.Version.VERSION_12);
+        Assert.assertTrue(valid);
+
+        final Bom bom = parser.parse(file);
+        testPedigree(bom.getComponents().get(0).getPedigree());
+    }
+
+    private void testPedigree(final Pedigree pedigree) {
+        Assert.assertEquals("sample-library-ancestor", pedigree.getAncestors().getComponents().get(0).getName());
+        Assert.assertEquals("sample-library-descendant", pedigree.getDescendants().getComponents().get(0).getName());
+        Assert.assertEquals("sample-library-variant", pedigree.getVariants().getComponents().get(0).getName());
+    }
+
     @Test
     public void testParsedObjects10Bom() throws Exception {
         final byte[] bomBytes = IOUtils.toByteArray(this.getClass().getResourceAsStream("/bom-1.0.xml"));
@@ -124,7 +149,8 @@ public class XmlParserTest {
         Assert.assertEquals("text/plain", c1.getLicenseChoice().getLicenses().get(0).getAttachmentText().getContentType());
         Assert.assertEquals("base64", c1.getLicenseChoice().getLicenses().get(0).getAttachmentText().getEncoding());
         Assert.assertNotNull(c1.getPedigree());
-        Assert.assertEquals(1, c1.getPedigree().getAncestors().size());
+
+        Assert.assertEquals(1, c1.getPedigree().getAncestors().getComponents().size());
         Assert.assertNull(c1.getPedigree().getDescendants());
         Assert.assertNull(c1.getPedigree().getVariants());
         Assert.assertEquals(1, c1.getPedigree().getCommits().size());
diff --git a/src/test/resources/bom-1.2-pedigree.xml b/src/test/resources/bom-1.2-pedigree.xml
new file mode 100644
index 000000000..31e7a3fa9
--- /dev/null
+++ b/src/test/resources/bom-1.2-pedigree.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<bom xmlns="http://cyclonedx.org/schema/bom/1.2"
+     serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"
+     version="1">
+  <components>
+    <component type="library">
+      <group>com.acme</group>
+      <name>sample-library</name>
+      <version>1.0.0</version>
+      <pedigree>
+        <ancestors>
+          <component type="library">
+            <group>org.example</group>
+            <name>sample-library-ancestor</name>
+            <version>1.0.0</version>
+          </component>
+        </ancestors>
+        <descendants>
+          <component type="library">
+            <group>org.example</group>
+            <name>sample-library-descendant</name>
+            <version>1.0.1</version>
+          </component>
+        </descendants>
+        <variants>
+          <component type="library">
+            <group>org.example</group>
+            <name>sample-library-variant</name>
+            <version>1.0.2</version>
+          </component>
+        </variants>
+      </pedigree>
+    </component>
+  </components>
+</bom>
diff --git a/src/test/resources/bom-1.2.json b/src/test/resources/bom-1.2.json
index 23ec2cd9f..e188a57b2 100644
--- a/src/test/resources/bom-1.2.json
+++ b/src/test/resources/bom-1.2.json
@@ -127,6 +127,38 @@
             "version": "9.0.14"
           }
         ],
+        "descendants": [
+          {
+            "type": "library",
+            "publisher": "Acme Inc",
+            "group": "com.acme",
+            "name": "tomcat-catalina",
+            "version": "9.0.14"
+          },
+          {
+            "type": "library",
+            "publisher": "Acme Inc",
+            "group": "com.acme",
+            "name": "tomcat-catalina",
+            "version": "9.0.14"
+          }
+        ],
+        "variants": [
+          {
+            "type": "library",
+            "publisher": "Acme Inc",
+            "group": "com.acme",
+            "name": "tomcat-catalina",
+            "version": "9.0.14"
+          },
+          {
+            "type": "library",
+            "publisher": "Acme Inc",
+            "group": "com.acme",
+            "name": "tomcat-catalina",
+            "version": "9.0.14"
+          }
+        ],
         "commits": [
           {
             "uid": "123",