From 4580fda3d0320e27e418a1110caae21bad0c292d Mon Sep 17 00:00:00 2001 From: Alex Alzate Date: Thu, 9 Apr 2026 10:21:56 -0500 Subject: [PATCH] Add OrganizationalContactsDeserializer and tests Introduce OrganizationalContactsDeserializer and wire it into Component#getAuthors to correctly parse author/author-list payloads within tool components. Refactor ToolDeserializer, ToolInformationDeserializer and ToolsDeserializer to use the JsonParser's ObjectMapper when available (via getMapper), ensuring consistent conversion behavior. Add regression tests and JSON/XML fixtures for issue815 to validate tool/component authors and external references parsing. --- .../java/org/cyclonedx/model/Component.java | 2 + .../OrganizationalContactsDeserializer.java | 59 +++++++++++++++++++ .../util/deserializer/ToolDeserializer.java | 11 +++- .../ToolInformationDeserializer.java | 23 +++++--- .../util/deserializer/ToolsDeserializer.java | 16 +++-- .../org/cyclonedx/parsers/JsonParserTest.java | 15 +++++ .../org/cyclonedx/parsers/XmlParserTest.java | 15 +++++ src/test/resources/regression/issue815.json | 29 +++++++++ src/test/resources/regression/issue815.xml | 24 ++++++++ 9 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/cyclonedx/util/deserializer/OrganizationalContactsDeserializer.java create mode 100644 src/test/resources/regression/issue815.json create mode 100644 src/test/resources/regression/issue815.xml diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 16c4718415..b9f0096d47 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -34,6 +34,7 @@ import org.cyclonedx.util.deserializer.ComponentListDeserializer; import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer; import org.cyclonedx.util.deserializer.HashesDeserializer; +import org.cyclonedx.util.deserializer.OrganizationalContactsDeserializer; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -580,6 +581,7 @@ public void setTags(final Tags tags) { * @since 1.6 */ @JsonGetter("authors") + @JsonDeserialize(using = OrganizationalContactsDeserializer.class) public List getAuthors() { return authors; } diff --git a/src/main/java/org/cyclonedx/util/deserializer/OrganizationalContactsDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalContactsDeserializer.java new file mode 100644 index 0000000000..fa063d7bfc --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalContactsDeserializer.java @@ -0,0 +1,59 @@ +/* + * 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) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.util.deserializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.cyclonedx.model.OrganizationalContact; + +public class OrganizationalContactsDeserializer + extends JsonDeserializer> +{ + @Override + public List deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonNode node = parser.getCodec().readTree(parser); + ObjectMapper mapper = getMapper(parser); + return parseContacts(node.has("author") ? node.get("author") : node, mapper); + } + + private List parseContacts(JsonNode node, ObjectMapper mapper) { + List contacts = new ArrayList<>(); + ArrayNode nodes = DeserializerUtils.getArrayNode(node, mapper); + for (JsonNode contactNode : nodes) { + contacts.add(mapper.convertValue(contactNode, OrganizationalContact.class)); + } + return contacts; + } + + private ObjectMapper getMapper(JsonParser jsonParser) { + if (jsonParser.getCodec() instanceof ObjectMapper) { + return (ObjectMapper) jsonParser.getCodec(); + } else { + return new ObjectMapper(); + } + } +} diff --git a/src/main/java/org/cyclonedx/util/deserializer/ToolDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/ToolDeserializer.java index a03eee5850..64f15dac93 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/ToolDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/ToolDeserializer.java @@ -31,12 +31,19 @@ public class ToolDeserializer extends JsonDeserializer { - private final ObjectMapper mapper = new ObjectMapper(); - @Override public Tool deserialize(JsonParser parser, DeserializationContext context) throws IOException { ObjectCodec codec = parser.getCodec(); JsonNode node = codec.readTree(parser); + ObjectMapper mapper = getMapper(parser); return mapper.convertValue(node, Tool.class); } + + private ObjectMapper getMapper(JsonParser jsonParser) { + if (jsonParser.getCodec() instanceof ObjectMapper) { + return (ObjectMapper) jsonParser.getCodec(); + } else { + return new ObjectMapper(); + } + } } diff --git a/src/main/java/org/cyclonedx/util/deserializer/ToolInformationDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/ToolInformationDeserializer.java index 3a9ce36232..96d0c166ba 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/ToolInformationDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/ToolInformationDeserializer.java @@ -34,28 +34,27 @@ public class ToolInformationDeserializer extends JsonDeserializer { - private final ObjectMapper mapper = new ObjectMapper(); - @Override public ToolInformation deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - return parseToolInformation(node); + ObjectMapper mapper = getMapper(jsonParser); + return parseToolInformation(node, mapper); } - private ToolInformation parseToolInformation(JsonNode toolsNode) { + private ToolInformation parseToolInformation(JsonNode toolsNode, ObjectMapper mapper) { ToolInformation toolInformation = new ToolInformation(); if (toolsNode.has("components")) { - parseComponents(toolsNode.get("components"), toolInformation); + parseComponents(toolsNode.get("components"), toolInformation, mapper); } if (toolsNode.has("services")) { - parseServices(toolsNode.get("services"), toolInformation); + parseServices(toolsNode.get("services"), toolInformation, mapper); } return toolInformation; } - private void parseComponents(JsonNode componentsNode, ToolInformation toolInformation) { + private void parseComponents(JsonNode componentsNode, ToolInformation toolInformation, ObjectMapper mapper) { if (componentsNode != null) { // Case JSON input where "components" is an array if (componentsNode.isArray()) { @@ -76,7 +75,7 @@ else if (componentsNode.isObject() && componentsNode.has("component")) { } } - private void parseServices(JsonNode servicesNode, ToolInformation toolInformation) { + private void parseServices(JsonNode servicesNode, ToolInformation toolInformation, ObjectMapper mapper) { if (servicesNode != null) { // Case JSON input where "services" is an array if (servicesNode.isArray()) { @@ -96,4 +95,12 @@ else if (servicesNode.isObject() && servicesNode.has("service")) { } } } + + private ObjectMapper getMapper(JsonParser jsonParser) { + if (jsonParser.getCodec() instanceof ObjectMapper) { + return (ObjectMapper) jsonParser.getCodec(); + } else { + return new ObjectMapper(); + } + } } diff --git a/src/main/java/org/cyclonedx/util/deserializer/ToolsDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/ToolsDeserializer.java index 2da349267d..0a42d4cb0c 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/ToolsDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/ToolsDeserializer.java @@ -34,17 +34,17 @@ public class ToolsDeserializer extends JsonDeserializer> { private final ToolDeserializer toolDeserializer = new ToolDeserializer(); - private final ObjectMapper objectMapper = new ObjectMapper(); @Override public List deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - return parseTools(node.has("tool") ? node.get("tool") : node, jsonParser, ctxt); + ObjectMapper mapper = getMapper(jsonParser); + return parseTools(node.has("tool") ? node.get("tool") : node, jsonParser, ctxt, mapper); } - private List parseTools(JsonNode node, JsonParser p, DeserializationContext ctxt) throws IOException { + private List parseTools(JsonNode node, JsonParser p, DeserializationContext ctxt, ObjectMapper mapper) throws IOException { List tools = new ArrayList<>(); - ArrayNode nodes = DeserializerUtils.getArrayNode(node, objectMapper); + ArrayNode nodes = DeserializerUtils.getArrayNode(node, mapper); for (JsonNode toolNode : nodes) { tools.add(parseTool(toolNode, p, ctxt)); } @@ -56,4 +56,12 @@ private Tool parseTool(JsonNode node, JsonParser p, DeserializationContext ctxt) toolParser.nextToken(); return toolDeserializer.deserialize(toolParser, ctxt); } + + private ObjectMapper getMapper(JsonParser jsonParser) { + if (jsonParser.getCodec() instanceof ObjectMapper) { + return (ObjectMapper) jsonParser.getCodec(); + } else { + return new ObjectMapper(); + } + } } diff --git a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java index d0efdbbea1..88ea902f1d 100644 --- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java @@ -596,6 +596,21 @@ public void testIssue562Regression() throws Exception { assertEquals(2, bom.getMetadata().getAuthors().size()); } + @Test + public void testIssue815Regression() throws Exception { + final Bom bom = getJsonBom("regression/issue815.json"); + assertNotNull(bom.getMetadata().getToolChoice()); + assertEquals(1, bom.getMetadata().getToolChoice().getComponents().size()); + Component tool = bom.getMetadata().getToolChoice().getComponents().get(0); + assertEquals("CycloneDX module for .NET", tool.getName()); + assertEquals("6.1.0.0", tool.getVersion()); + assertNotNull(tool.getAuthors()); + assertEquals(1, tool.getAuthors().size()); + assertEquals("CycloneDX", tool.getAuthors().get(0).getName()); + assertNotNull(tool.getExternalReferences()); + assertEquals(1, tool.getExternalReferences().size()); + } + @Test public void testIssue492Regression() throws Exception { final Bom bom = getJsonBom("regression/issue492.json"); diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java index 0074cdbd32..7dea9e3720 100644 --- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java @@ -745,6 +745,21 @@ public void testIssue562Regression() throws Exception { assertEquals(2, bom.getMetadata().getAuthors().size()); } + @Test + public void testIssue815Regression() throws Exception { + final Bom bom = getXmlBom("regression/issue815.xml"); + assertNotNull(bom.getMetadata().getToolChoice()); + assertEquals(1, bom.getMetadata().getToolChoice().getComponents().size()); + Component tool = bom.getMetadata().getToolChoice().getComponents().get(0); + assertEquals("CycloneDX module for .NET", tool.getName()); + assertEquals("6.1.0.0", tool.getVersion()); + assertNotNull(tool.getAuthors()); + assertEquals(1, tool.getAuthors().size()); + assertEquals("CycloneDX", tool.getAuthors().get(0).getName()); + assertNotNull(tool.getExternalReferences()); + assertEquals(1, tool.getExternalReferences().size()); + } + @Test public void testIssue492Regression() throws Exception { final Bom bom = getXmlBom("regression/issue492.xml"); diff --git a/src/test/resources/regression/issue815.json b/src/test/resources/regression/issue815.json new file mode 100644 index 0000000000..7e433d5331 --- /dev/null +++ b/src/test/resources/regression/issue815.json @@ -0,0 +1,29 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:134bdd62-7b55-4f31-bc92-583aeaac3b29", + "version": 1, + "metadata": { + "timestamp": "2026-04-01T08:22:04Z", + "tools": { + "components": [ + { + "type": "application", + "authors": [ + { + "name": "CycloneDX" + } + ], + "name": "CycloneDX module for .NET", + "version": "6.1.0.0", + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/CycloneDX/cyclonedx-dotnet" + } + ] + } + ] + } + } +} diff --git a/src/test/resources/regression/issue815.xml b/src/test/resources/regression/issue815.xml new file mode 100644 index 0000000000..88429b6f5d --- /dev/null +++ b/src/test/resources/regression/issue815.xml @@ -0,0 +1,24 @@ + + + + 2026-04-01T08:22:04Z + + + + + + CycloneDX + + + CycloneDX module for .NET + 6.1.0.0 + + + https://github.com/CycloneDX/cyclonedx-dotnet + + + + + + +