Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/java/org/cyclonedx/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -580,6 +581,7 @@ public void setTags(final Tags tags) {
* @since 1.6
*/
@JsonGetter("authors")
@JsonDeserialize(using = OrganizationalContactsDeserializer.class)
public List<OrganizationalContact> getAuthors() {
return authors;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<OrganizationalContact>>
{
@Override
public List<OrganizationalContact> 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<OrganizationalContact> parseContacts(JsonNode node, ObjectMapper mapper) {
List<OrganizationalContact> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,19 @@
public class ToolDeserializer
extends JsonDeserializer<Tool>
{
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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,27 @@
public class ToolInformationDeserializer
extends JsonDeserializer<ToolInformation>
{
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()) {
Expand All @@ -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()) {
Expand All @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,17 @@ public class ToolsDeserializer
extends JsonDeserializer<List<Tool>>
{
private final ToolDeserializer toolDeserializer = new ToolDeserializer();
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public List<Tool> 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<Tool> parseTools(JsonNode node, JsonParser p, DeserializationContext ctxt) throws IOException {
private List<Tool> parseTools(JsonNode node, JsonParser p, DeserializationContext ctxt, ObjectMapper mapper) throws IOException {
List<Tool> 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));
}
Expand All @@ -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();
}
}
}
15 changes: 15 additions & 0 deletions src/test/java/org/cyclonedx/parsers/JsonParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
15 changes: 15 additions & 0 deletions src/test/java/org/cyclonedx/parsers/XmlParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
29 changes: 29 additions & 0 deletions src/test/resources/regression/issue815.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
]
}
}
}
24 changes: 24 additions & 0 deletions src/test/resources/regression/issue815.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.6" serialNumber="urn:uuid:134bdd62-7b55-4f31-bc92-583aeaac3b29" version="1">
<metadata>
<timestamp>2026-04-01T08:22:04Z</timestamp>
<tools>
<components>
<component type="application">
<authors>
<author>
<name>CycloneDX</name>
</author>
</authors>
<name>CycloneDX module for .NET</name>
<version>6.1.0.0</version>
<externalReferences>
<reference type="website">
<url>https://github.com/CycloneDX/cyclonedx-dotnet</url>
</reference>
</externalReferences>
</component>
</components>
</tools>
</metadata>
</bom>
Loading