Skip to content

Commit a0e854d

Browse files
committed
Refactor author/authors serialization for Component
Introduces custom serializers to resolve naming conflicts between the deprecated 'author' string and the new 'authors' list fields in Component. Adds AuthorsBeanSerializerModifier and AuthorsSerializer to ensure correct XML/JSON output across CycloneDX schema versions. Updates OrganizationalContact and Component annotations for precise serialization. Includes comprehensive tests for all field combinations, formats, and migration scenarios.
1 parent f4fd4b0 commit a0e854d

File tree

9 files changed

+1097
-2
lines changed

9 files changed

+1097
-2
lines changed

src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.cyclonedx.Format;
77
import org.cyclonedx.Version;
88
import org.cyclonedx.model.Bom;
9+
import org.cyclonedx.util.serializer.AuthorsBeanSerializerModifier;
910
import org.cyclonedx.util.serializer.CustomSerializerModifier;
1011
import org.cyclonedx.util.serializer.EvidenceSerializer;
1112
import org.cyclonedx.util.serializer.ExternalReferenceSerializer;
@@ -93,6 +94,10 @@ protected void setupObjectMapper(boolean isXml) {
9394
hash1Module.addSerializer(new HashSerializer(version));
9495
mapper.registerModule(hash1Module);
9596

97+
SimpleModule authorsModule = new SimpleModule();
98+
authorsModule.setSerializerModifier(new AuthorsBeanSerializerModifier(version));
99+
mapper.registerModule(authorsModule);
100+
96101
SimpleModule propertiesModule = new SimpleModule();
97102
propertiesModule.setSerializerModifier(new CustomSerializerModifier(isXml, version));
98103
mapper.registerModule(propertiesModule);

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import java.util.List;
2323
import java.util.Objects;
2424

25+
import com.fasterxml.jackson.annotation.JsonGetter;
2526
import com.fasterxml.jackson.annotation.JsonIgnore;
27+
import com.fasterxml.jackson.annotation.JsonSetter;
2628
import com.fasterxml.jackson.annotation.JsonUnwrapped;
2729
import org.cyclonedx.Version;
2830
import org.cyclonedx.model.component.ModelCard;
@@ -223,7 +225,6 @@ public String getScopeName() {
223225
private Tags tags;
224226

225227
@VersionFilter(Version.VERSION_16)
226-
@JsonProperty("authors")
227228
private List<OrganizationalContact> authors;
228229

229230
@VersionFilter(Version.VERSION_16)
@@ -258,10 +259,26 @@ public void setSupplier(OrganizationalEntity supplier) {
258259
this.supplier = supplier;
259260
}
260261

262+
/**
263+
* Gets the deprecated author field as a string.
264+
* @return the author name as a string
265+
* @deprecated since version 1.6, use {@link #getAuthors()} instead
266+
*/
267+
@Deprecated
268+
@JsonGetter("author")
269+
@JacksonXmlProperty(localName = "author")
261270
public String getAuthor() {
262271
return author;
263272
}
264273

274+
/**
275+
* Sets the deprecated author field as a string.
276+
* @param author the author name as a string
277+
* @deprecated since version 1.6, use {@link #setAuthors(List)} instead
278+
*/
279+
@Deprecated
280+
@JsonSetter("author")
281+
@JacksonXmlProperty(localName = "author")
265282
public void setAuthor(String author) {
266283
this.author = author;
267284
}
@@ -556,10 +573,24 @@ public void setTags(final Tags tags) {
556573
this.tags = tags;
557574
}
558575

576+
/**
577+
* Gets the component authors as a list of contacts.
578+
* This replaces the deprecated string-based author field.
579+
* @return the list of authors, or null if not set
580+
* @since 1.6
581+
*/
582+
@JsonGetter("authors")
559583
public List<OrganizationalContact> getAuthors() {
560584
return authors;
561585
}
562586

587+
/**
588+
* Sets the component authors as a list of contacts.
589+
* This replaces the deprecated string-based author field.
590+
* @param authors the list of authors
591+
* @since 1.6
592+
*/
593+
@JsonSetter("authors")
563594
public void setAuthors(final List<OrganizationalContact> authors) {
564595
this.authors = authors;
565596
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
import com.fasterxml.jackson.annotation.JsonProperty;
2525
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
2626
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
27+
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
2728
import org.cyclonedx.Version;
2829

2930
import java.util.Objects;
3031

32+
@JacksonXmlRootElement(localName = "author")
3133
@JsonIgnoreProperties(ignoreUnknown = true)
3234
@JsonInclude(Include.NON_EMPTY)
3335
@JsonPropertyOrder({"name", "email", "phone"})

src/main/java/org/cyclonedx/util/introspector/VersionJsonAnnotationIntrospector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public boolean hasIgnoreMarker(final AnnotatedMember m) {
4747

4848
// Check if the field has the XmlOnly annotation
4949
if (m.hasAnnotation(XmlOnly.class)) {
50-
// If true, the field should be ignored for XML serialization
50+
// If true, the field should be ignored for JSON serialization
5151
return true;
5252
}
5353

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.serializer;
20+
21+
import com.fasterxml.jackson.databind.BeanDescription;
22+
import com.fasterxml.jackson.databind.JsonSerializer;
23+
import com.fasterxml.jackson.databind.SerializationConfig;
24+
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
25+
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
26+
import org.cyclonedx.Version;
27+
import org.cyclonedx.model.Component;
28+
29+
import java.util.List;
30+
31+
/**
32+
* Bean serializer modifier for Component.authors field.
33+
* Applies the AuthorsSerializer only to the authors field in Component class.
34+
*/
35+
public class AuthorsBeanSerializerModifier extends BeanSerializerModifier {
36+
private final Version version;
37+
38+
public AuthorsBeanSerializerModifier(Version version) {
39+
this.version = version;
40+
}
41+
42+
@Override
43+
public List<BeanPropertyWriter> changeProperties(
44+
SerializationConfig config,
45+
BeanDescription beanDesc,
46+
List<BeanPropertyWriter> beanProperties) {
47+
48+
// Only modify Component class
49+
if (Component.class.isAssignableFrom(beanDesc.getBeanClass())) {
50+
java.util.Iterator<BeanPropertyWriter> iterator = beanProperties.iterator();
51+
while (iterator.hasNext()) {
52+
BeanPropertyWriter writer = iterator.next();
53+
// Find the authors property
54+
if ("authors".equals(writer.getName())) {
55+
// Check if the current version supports the authors field (v1.6+)
56+
if (version.getVersion() < Version.VERSION_16.getVersion()) {
57+
// Remove the property for versions earlier than 1.6
58+
iterator.remove();
59+
} else {
60+
// Assign the custom serializer for v1.6+
61+
JsonSerializer<?> serializer = new AuthorsSerializer();
62+
writer.assignSerializer((JsonSerializer<Object>) serializer);
63+
}
64+
break;
65+
}
66+
}
67+
}
68+
return beanProperties;
69+
}
70+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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.serializer;
20+
21+
import com.fasterxml.jackson.core.JsonGenerator;
22+
import com.fasterxml.jackson.databind.SerializerProvider;
23+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
24+
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
25+
import org.cyclonedx.model.OrganizationalContact;
26+
27+
import java.io.IOException;
28+
import java.util.List;
29+
30+
/**
31+
* Custom serializer for the Component.authors field to handle XML element naming.
32+
* This serializer ensures that:
33+
* - JSON: serializes as "authors": [...]
34+
* - XML: serializes as authors->author
35+
* This is necessary because the deprecated "author" field also uses author element,
36+
* creating a naming conflict that standard Jackson annotations cannot resolve.
37+
* Version filtering is handled by AuthorsBeanSerializerModifier, which removes this
38+
* property entirely for versions prior to 1.6.
39+
*/
40+
public class AuthorsSerializer extends StdSerializer<List<OrganizationalContact>> {
41+
42+
@SuppressWarnings("unchecked")
43+
public AuthorsSerializer() {
44+
super((Class<List<OrganizationalContact>>) (Class<?>) List.class);
45+
}
46+
47+
@Override
48+
public void serialize(List<OrganizationalContact> authors, JsonGenerator gen, SerializerProvider serializers)
49+
throws IOException {
50+
51+
if (authors == null || authors.isEmpty()) {
52+
return;
53+
}
54+
55+
// Check if we're serializing to XML
56+
if (gen instanceof ToXmlGenerator) {
57+
ToXmlGenerator xmlGen = (ToXmlGenerator) gen;
58+
59+
// For XML: The property name "authors" creates the wrapper <authors>
60+
// We need to write the array structure, and each item gets its own <author> tag
61+
xmlGen.writeStartArray();
62+
for (OrganizationalContact contact : authors) {
63+
// Set the element name for this array item to "author"
64+
xmlGen.setNextName(new javax.xml.namespace.QName("author"));
65+
serializers.defaultSerializeValue(contact, xmlGen);
66+
}
67+
xmlGen.writeEndArray();
68+
} else {
69+
// For JSON, write as a standard array
70+
gen.writeStartArray();
71+
for (OrganizationalContact contact : authors) {
72+
serializers.defaultSerializeValue(contact, gen);
73+
}
74+
gen.writeEndArray();
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)