Skip to content

Commit 4580fda

Browse files
committed
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.
1 parent eda3cad commit 4580fda

9 files changed

Lines changed: 180 additions & 14 deletions

File tree

src/main/java/org/cyclonedx/model/Component.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.cyclonedx.util.deserializer.ComponentListDeserializer;
3535
import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer;
3636
import org.cyclonedx.util.deserializer.HashesDeserializer;
37+
import org.cyclonedx.util.deserializer.OrganizationalContactsDeserializer;
3738
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
3839
import com.fasterxml.jackson.annotation.JsonInclude;
3940
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -580,6 +581,7 @@ public void setTags(final Tags tags) {
580581
* @since 1.6
581582
*/
582583
@JsonGetter("authors")
584+
@JsonDeserialize(using = OrganizationalContactsDeserializer.class)
583585
public List<OrganizationalContact> getAuthors() {
584586
return authors;
585587
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* This file is part of CycloneDX Core (Java).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* Copyright (c) OWASP Foundation. All Rights Reserved.
18+
*/
19+
package org.cyclonedx.util.deserializer;
20+
21+
import java.io.IOException;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
import com.fasterxml.jackson.core.JsonParser;
26+
import com.fasterxml.jackson.databind.DeserializationContext;
27+
import com.fasterxml.jackson.databind.JsonDeserializer;
28+
import com.fasterxml.jackson.databind.JsonNode;
29+
import com.fasterxml.jackson.databind.ObjectMapper;
30+
import com.fasterxml.jackson.databind.node.ArrayNode;
31+
import org.cyclonedx.model.OrganizationalContact;
32+
33+
public class OrganizationalContactsDeserializer
34+
extends JsonDeserializer<List<OrganizationalContact>>
35+
{
36+
@Override
37+
public List<OrganizationalContact> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
38+
JsonNode node = parser.getCodec().readTree(parser);
39+
ObjectMapper mapper = getMapper(parser);
40+
return parseContacts(node.has("author") ? node.get("author") : node, mapper);
41+
}
42+
43+
private List<OrganizationalContact> parseContacts(JsonNode node, ObjectMapper mapper) {
44+
List<OrganizationalContact> contacts = new ArrayList<>();
45+
ArrayNode nodes = DeserializerUtils.getArrayNode(node, mapper);
46+
for (JsonNode contactNode : nodes) {
47+
contacts.add(mapper.convertValue(contactNode, OrganizationalContact.class));
48+
}
49+
return contacts;
50+
}
51+
52+
private ObjectMapper getMapper(JsonParser jsonParser) {
53+
if (jsonParser.getCodec() instanceof ObjectMapper) {
54+
return (ObjectMapper) jsonParser.getCodec();
55+
} else {
56+
return new ObjectMapper();
57+
}
58+
}
59+
}

src/main/java/org/cyclonedx/util/deserializer/ToolDeserializer.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,19 @@
3131
public class ToolDeserializer
3232
extends JsonDeserializer<Tool>
3333
{
34-
private final ObjectMapper mapper = new ObjectMapper();
35-
3634
@Override
3735
public Tool deserialize(JsonParser parser, DeserializationContext context) throws IOException {
3836
ObjectCodec codec = parser.getCodec();
3937
JsonNode node = codec.readTree(parser);
38+
ObjectMapper mapper = getMapper(parser);
4039
return mapper.convertValue(node, Tool.class);
4140
}
41+
42+
private ObjectMapper getMapper(JsonParser jsonParser) {
43+
if (jsonParser.getCodec() instanceof ObjectMapper) {
44+
return (ObjectMapper) jsonParser.getCodec();
45+
} else {
46+
return new ObjectMapper();
47+
}
48+
}
4249
}

src/main/java/org/cyclonedx/util/deserializer/ToolInformationDeserializer.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,27 @@
3434
public class ToolInformationDeserializer
3535
extends JsonDeserializer<ToolInformation>
3636
{
37-
private final ObjectMapper mapper = new ObjectMapper();
38-
3937
@Override
4038
public ToolInformation deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
4139
throws IOException
4240
{
4341
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
44-
return parseToolInformation(node);
42+
ObjectMapper mapper = getMapper(jsonParser);
43+
return parseToolInformation(node, mapper);
4544
}
4645

47-
private ToolInformation parseToolInformation(JsonNode toolsNode) {
46+
private ToolInformation parseToolInformation(JsonNode toolsNode, ObjectMapper mapper) {
4847
ToolInformation toolInformation = new ToolInformation();
4948
if (toolsNode.has("components")) {
50-
parseComponents(toolsNode.get("components"), toolInformation);
49+
parseComponents(toolsNode.get("components"), toolInformation, mapper);
5150
}
5251
if (toolsNode.has("services")) {
53-
parseServices(toolsNode.get("services"), toolInformation);
52+
parseServices(toolsNode.get("services"), toolInformation, mapper);
5453
}
5554
return toolInformation;
5655
}
5756

58-
private void parseComponents(JsonNode componentsNode, ToolInformation toolInformation) {
57+
private void parseComponents(JsonNode componentsNode, ToolInformation toolInformation, ObjectMapper mapper) {
5958
if (componentsNode != null) {
6059
// Case JSON input where "components" is an array
6160
if (componentsNode.isArray()) {
@@ -76,7 +75,7 @@ else if (componentsNode.isObject() && componentsNode.has("component")) {
7675
}
7776
}
7877

79-
private void parseServices(JsonNode servicesNode, ToolInformation toolInformation) {
78+
private void parseServices(JsonNode servicesNode, ToolInformation toolInformation, ObjectMapper mapper) {
8079
if (servicesNode != null) {
8180
// Case JSON input where "services" is an array
8281
if (servicesNode.isArray()) {
@@ -96,4 +95,12 @@ else if (servicesNode.isObject() && servicesNode.has("service")) {
9695
}
9796
}
9897
}
98+
99+
private ObjectMapper getMapper(JsonParser jsonParser) {
100+
if (jsonParser.getCodec() instanceof ObjectMapper) {
101+
return (ObjectMapper) jsonParser.getCodec();
102+
} else {
103+
return new ObjectMapper();
104+
}
105+
}
99106
}

src/main/java/org/cyclonedx/util/deserializer/ToolsDeserializer.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@ public class ToolsDeserializer
3434
extends JsonDeserializer<List<Tool>>
3535
{
3636
private final ToolDeserializer toolDeserializer = new ToolDeserializer();
37-
private final ObjectMapper objectMapper = new ObjectMapper();
3837

3938
@Override
4039
public List<Tool> deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
4140
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
42-
return parseTools(node.has("tool") ? node.get("tool") : node, jsonParser, ctxt);
41+
ObjectMapper mapper = getMapper(jsonParser);
42+
return parseTools(node.has("tool") ? node.get("tool") : node, jsonParser, ctxt, mapper);
4343
}
4444

45-
private List<Tool> parseTools(JsonNode node, JsonParser p, DeserializationContext ctxt) throws IOException {
45+
private List<Tool> parseTools(JsonNode node, JsonParser p, DeserializationContext ctxt, ObjectMapper mapper) throws IOException {
4646
List<Tool> tools = new ArrayList<>();
47-
ArrayNode nodes = DeserializerUtils.getArrayNode(node, objectMapper);
47+
ArrayNode nodes = DeserializerUtils.getArrayNode(node, mapper);
4848
for (JsonNode toolNode : nodes) {
4949
tools.add(parseTool(toolNode, p, ctxt));
5050
}
@@ -56,4 +56,12 @@ private Tool parseTool(JsonNode node, JsonParser p, DeserializationContext ctxt)
5656
toolParser.nextToken();
5757
return toolDeserializer.deserialize(toolParser, ctxt);
5858
}
59+
60+
private ObjectMapper getMapper(JsonParser jsonParser) {
61+
if (jsonParser.getCodec() instanceof ObjectMapper) {
62+
return (ObjectMapper) jsonParser.getCodec();
63+
} else {
64+
return new ObjectMapper();
65+
}
66+
}
5967
}

src/test/java/org/cyclonedx/parsers/JsonParserTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,21 @@ public void testIssue562Regression() throws Exception {
596596
assertEquals(2, bom.getMetadata().getAuthors().size());
597597
}
598598

599+
@Test
600+
public void testIssue815Regression() throws Exception {
601+
final Bom bom = getJsonBom("regression/issue815.json");
602+
assertNotNull(bom.getMetadata().getToolChoice());
603+
assertEquals(1, bom.getMetadata().getToolChoice().getComponents().size());
604+
Component tool = bom.getMetadata().getToolChoice().getComponents().get(0);
605+
assertEquals("CycloneDX module for .NET", tool.getName());
606+
assertEquals("6.1.0.0", tool.getVersion());
607+
assertNotNull(tool.getAuthors());
608+
assertEquals(1, tool.getAuthors().size());
609+
assertEquals("CycloneDX", tool.getAuthors().get(0).getName());
610+
assertNotNull(tool.getExternalReferences());
611+
assertEquals(1, tool.getExternalReferences().size());
612+
}
613+
599614
@Test
600615
public void testIssue492Regression() throws Exception {
601616
final Bom bom = getJsonBom("regression/issue492.json");

src/test/java/org/cyclonedx/parsers/XmlParserTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,21 @@ public void testIssue562Regression() throws Exception {
745745
assertEquals(2, bom.getMetadata().getAuthors().size());
746746
}
747747

748+
@Test
749+
public void testIssue815Regression() throws Exception {
750+
final Bom bom = getXmlBom("regression/issue815.xml");
751+
assertNotNull(bom.getMetadata().getToolChoice());
752+
assertEquals(1, bom.getMetadata().getToolChoice().getComponents().size());
753+
Component tool = bom.getMetadata().getToolChoice().getComponents().get(0);
754+
assertEquals("CycloneDX module for .NET", tool.getName());
755+
assertEquals("6.1.0.0", tool.getVersion());
756+
assertNotNull(tool.getAuthors());
757+
assertEquals(1, tool.getAuthors().size());
758+
assertEquals("CycloneDX", tool.getAuthors().get(0).getName());
759+
assertNotNull(tool.getExternalReferences());
760+
assertEquals(1, tool.getExternalReferences().size());
761+
}
762+
748763
@Test
749764
public void testIssue492Regression() throws Exception {
750765
final Bom bom = getXmlBom("regression/issue492.xml");
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.6",
4+
"serialNumber": "urn:uuid:134bdd62-7b55-4f31-bc92-583aeaac3b29",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2026-04-01T08:22:04Z",
8+
"tools": {
9+
"components": [
10+
{
11+
"type": "application",
12+
"authors": [
13+
{
14+
"name": "CycloneDX"
15+
}
16+
],
17+
"name": "CycloneDX module for .NET",
18+
"version": "6.1.0.0",
19+
"externalReferences": [
20+
{
21+
"type": "website",
22+
"url": "https://github.com/CycloneDX/cyclonedx-dotnet"
23+
}
24+
]
25+
}
26+
]
27+
}
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="urn:uuid:134bdd62-7b55-4f31-bc92-583aeaac3b29" version="1">
3+
<metadata>
4+
<timestamp>2026-04-01T08:22:04Z</timestamp>
5+
<tools>
6+
<components>
7+
<component type="application">
8+
<authors>
9+
<author>
10+
<name>CycloneDX</name>
11+
</author>
12+
</authors>
13+
<name>CycloneDX module for .NET</name>
14+
<version>6.1.0.0</version>
15+
<externalReferences>
16+
<reference type="website">
17+
<url>https://github.com/CycloneDX/cyclonedx-dotnet</url>
18+
</reference>
19+
</externalReferences>
20+
</component>
21+
</components>
22+
</tools>
23+
</metadata>
24+
</bom>

0 commit comments

Comments
 (0)