diff --git a/cyclonedx/serialization/__init__.py b/cyclonedx/serialization/__init__.py
index 49eed8ff8..eb15b17a4 100644
--- a/cyclonedx/serialization/__init__.py
+++ b/cyclonedx/serialization/__init__.py
@@ -91,9 +91,10 @@ def deserialize(cls, o: object) -> SortedSet[LicenseChoice]:
@classmethod
def serialize(cls, o: object) -> List[LicenseChoice]:
+ # need to call `list(o)`, because `o` could be any iterable.
licenses: List[LicenseChoice] = list(o) # type: ignore[call-overload]
if len(licenses) > 1:
- expression = next(license for license in licenses if license.expression)
+ expression = next((l for l in licenses if l.expression), None)
if expression:
warnings.warn(
f'Licenses: found an expression {expression!r}, dropping the rest of: {licenses!r}',
diff --git a/tests/fixtures/json/1.2/regression365_expression-preferred.json b/tests/fixtures/json/1.2/regression365_expression-preferred.json
new file mode 100644
index 000000000..edcf9c054
--- /dev/null
+++ b/tests/fixtures/json/1.2/regression365_expression-preferred.json
@@ -0,0 +1 @@
+{"components": [{"bom-ref": "testing", "licenses": [{"expression": "(Apache-2.0 OR MIT)"}], "name": "expression-preferred", "type": "library", "version": ""}], "dependencies": [{"ref": "testing"}], "metadata": {"timestamp": "2022-06-15T13:09:38+00:00", "tools": [{"name": "cyclonedx-python-lib", "vendor": "CycloneDX", "version": "4.0.0"}]}, "serialNumber": "urn:uuid:66f6f3d4-0d24-4db3-b69c-bd547be9b0d3", "version": 1, "$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.2"}
\ No newline at end of file
diff --git a/tests/fixtures/json/1.2/regression365_multiple-licenses.json b/tests/fixtures/json/1.2/regression365_multiple-licenses.json
new file mode 100644
index 000000000..3c72b8260
--- /dev/null
+++ b/tests/fixtures/json/1.2/regression365_multiple-licenses.json
@@ -0,0 +1 @@
+{"components": [{"bom-ref": "testing", "licenses": [{"license": {"id": "Apache-2.0", "text": {"content": "VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=", "contentType": "text/plain", "encoding": "base64"}, "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}}, {"license": {"name": "OSI_APACHE"}}], "name": "multiple-licenses", "type": "library", "version": ""}], "dependencies": [{"ref": "testing"}], "metadata": {"timestamp": "2022-06-15T13:05:12+00:00", "tools": [{"name": "cyclonedx-python-lib", "vendor": "CycloneDX", "version": "4.0.0"}]}, "serialNumber": "urn:uuid:92f71d34-625a-4497-9891-3333c56a7af1", "version": 1, "$schema": "http://cyclonedx.org/schema/bom-1.2b.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.2"}
\ No newline at end of file
diff --git a/tests/fixtures/json/1.3/regression365_expression-preferred.json b/tests/fixtures/json/1.3/regression365_expression-preferred.json
new file mode 100644
index 000000000..fb095a230
--- /dev/null
+++ b/tests/fixtures/json/1.3/regression365_expression-preferred.json
@@ -0,0 +1 @@
+{"components": [{"bom-ref": "testing", "licenses": [{"expression": "(Apache-2.0 OR MIT)"}], "name": "expression-preferred", "type": "library", "version": ""}], "dependencies": [{"ref": "testing"}], "metadata": {"timestamp": "2022-06-15T13:09:38+00:00", "tools": [{"name": "cyclonedx-python-lib", "vendor": "CycloneDX", "version": "4.0.0"}]}, "serialNumber": "urn:uuid:66f6f3d4-0d24-4db3-b69c-bd547be9b0d3", "version": 1, "$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.3"}
\ No newline at end of file
diff --git a/tests/fixtures/json/1.3/regression365_multiple-licenses.json b/tests/fixtures/json/1.3/regression365_multiple-licenses.json
new file mode 100644
index 000000000..fd11ced50
--- /dev/null
+++ b/tests/fixtures/json/1.3/regression365_multiple-licenses.json
@@ -0,0 +1 @@
+{"components": [{"bom-ref": "testing", "licenses": [{"license": {"id": "Apache-2.0", "text": {"content": "VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=", "contentType": "text/plain", "encoding": "base64"}, "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}}, {"license": {"name": "OSI_APACHE"}}], "name": "multiple-licenses", "type": "library", "version": ""}], "dependencies": [{"ref": "testing"}], "metadata": {"timestamp": "2022-06-15T13:05:12+00:00", "tools": [{"name": "cyclonedx-python-lib", "vendor": "CycloneDX", "version": "4.0.0"}]}, "serialNumber": "urn:uuid:92f71d34-625a-4497-9891-3333c56a7af1", "version": 1, "$schema": "http://cyclonedx.org/schema/bom-1.3a.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.3"}
\ No newline at end of file
diff --git a/tests/fixtures/json/1.4/regression365_expression-preferred.json b/tests/fixtures/json/1.4/regression365_expression-preferred.json
new file mode 100644
index 000000000..dbe3ea3ff
--- /dev/null
+++ b/tests/fixtures/json/1.4/regression365_expression-preferred.json
@@ -0,0 +1 @@
+{"components": [{"bom-ref": "testing", "licenses": [{"expression": "(Apache-2.0 OR MIT)"}], "name": "expression-preferred", "type": "library"}], "dependencies": [{"ref": "testing"}], "metadata": {"timestamp": "2022-06-15T13:09:38+00:00", "tools": [{"externalReferences": [{"type": "build-system", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"}, {"type": "distribution", "url": "https://pypi.org/project/cyclonedx-python-lib/"}, {"type": "documentation", "url": "https://cyclonedx.github.io/cyclonedx-python-lib/"}, {"type": "issue-tracker", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"}, {"type": "license", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"}, {"type": "release-notes", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"}, {"type": "vcs", "url": "https://github.com/CycloneDX/cyclonedx-python-lib"}, {"type": "website", "url": "https://cyclonedx.org"}], "name": "cyclonedx-python-lib", "vendor": "CycloneDX", "version": "4.0.0"}]}, "serialNumber": "urn:uuid:66f6f3d4-0d24-4db3-b69c-bd547be9b0d3", "version": 1, "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.4"}
\ No newline at end of file
diff --git a/tests/fixtures/json/1.4/regression365_multiple-licenses.json b/tests/fixtures/json/1.4/regression365_multiple-licenses.json
new file mode 100644
index 000000000..8b2123e4f
--- /dev/null
+++ b/tests/fixtures/json/1.4/regression365_multiple-licenses.json
@@ -0,0 +1 @@
+{"components": [{"bom-ref": "testing", "licenses": [{"license": {"id": "Apache-2.0", "text": {"content": "VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=", "contentType": "text/plain", "encoding": "base64"}, "url": "https://www.apache.org/licenses/LICENSE-2.0.txt"}}, {"license": {"name": "OSI_APACHE"}}], "name": "multiple-licenses", "type": "library"}], "dependencies": [{"ref": "testing"}], "metadata": {"timestamp": "2022-06-15T13:05:12+00:00", "tools": [{"externalReferences": [{"type": "build-system", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/actions"}, {"type": "distribution", "url": "https://pypi.org/project/cyclonedx-python-lib/"}, {"type": "documentation", "url": "https://cyclonedx.github.io/cyclonedx-python-lib/"}, {"type": "issue-tracker", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/issues"}, {"type": "license", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSE"}, {"type": "release-notes", "url": "https://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.md"}, {"type": "vcs", "url": "https://github.com/CycloneDX/cyclonedx-python-lib"}, {"type": "website", "url": "https://cyclonedx.org"}], "name": "cyclonedx-python-lib", "vendor": "CycloneDX", "version": "4.0.0"}]}, "serialNumber": "urn:uuid:92f71d34-625a-4497-9891-3333c56a7af1", "version": 1, "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.4"}
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.0/regression365_expression-preferred.xml b/tests/fixtures/xml/1.0/regression365_expression-preferred.xml
new file mode 100644
index 000000000..057ae3fdf
--- /dev/null
+++ b/tests/fixtures/xml/1.0/regression365_expression-preferred.xml
@@ -0,0 +1 @@
+expression-preferredfalse
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.0/regression365_multiple-licenses.xml b/tests/fixtures/xml/1.0/regression365_multiple-licenses.xml
new file mode 100644
index 000000000..860c56d74
--- /dev/null
+++ b/tests/fixtures/xml/1.0/regression365_multiple-licenses.xml
@@ -0,0 +1 @@
+multiple-licensesfalse
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.1/regression365_expression-preferred.xml b/tests/fixtures/xml/1.1/regression365_expression-preferred.xml
new file mode 100644
index 000000000..9ce805105
--- /dev/null
+++ b/tests/fixtures/xml/1.1/regression365_expression-preferred.xml
@@ -0,0 +1 @@
+expression-preferredApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE(Apache-2.0 OR MIT)
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.1/regression365_multiple-licenses.xml b/tests/fixtures/xml/1.1/regression365_multiple-licenses.xml
new file mode 100644
index 000000000..a429f11fc
--- /dev/null
+++ b/tests/fixtures/xml/1.1/regression365_multiple-licenses.xml
@@ -0,0 +1 @@
+multiple-licensesApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.2/regression365_expression-preferred.xml b/tests/fixtures/xml/1.2/regression365_expression-preferred.xml
new file mode 100644
index 000000000..c77bf6254
--- /dev/null
+++ b/tests/fixtures/xml/1.2/regression365_expression-preferred.xml
@@ -0,0 +1 @@
+2022-06-15T13:09:38+00:00CycloneDXcyclonedx-python-lib4.0.0expression-preferredApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE(Apache-2.0 OR MIT)
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.2/regression365_multiple-licenses.xml b/tests/fixtures/xml/1.2/regression365_multiple-licenses.xml
new file mode 100644
index 000000000..d3fb2f8fe
--- /dev/null
+++ b/tests/fixtures/xml/1.2/regression365_multiple-licenses.xml
@@ -0,0 +1 @@
+2022-06-15T13:05:12+00:00CycloneDXcyclonedx-python-lib4.0.0multiple-licensesApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.3/regression365_expression-preferred.xml b/tests/fixtures/xml/1.3/regression365_expression-preferred.xml
new file mode 100644
index 000000000..34e50421b
--- /dev/null
+++ b/tests/fixtures/xml/1.3/regression365_expression-preferred.xml
@@ -0,0 +1 @@
+2022-06-15T13:09:38+00:00CycloneDXcyclonedx-python-lib4.0.0expression-preferredApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE(Apache-2.0 OR MIT)
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.3/regression365_multiple-licenses.xml b/tests/fixtures/xml/1.3/regression365_multiple-licenses.xml
new file mode 100644
index 000000000..78ffa14fe
--- /dev/null
+++ b/tests/fixtures/xml/1.3/regression365_multiple-licenses.xml
@@ -0,0 +1 @@
+2022-06-15T13:05:12+00:00CycloneDXcyclonedx-python-lib4.0.0multiple-licensesApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.4/regression365_expression-preferred.xml b/tests/fixtures/xml/1.4/regression365_expression-preferred.xml
new file mode 100644
index 000000000..2be73645b
--- /dev/null
+++ b/tests/fixtures/xml/1.4/regression365_expression-preferred.xml
@@ -0,0 +1 @@
+2022-06-15T13:09:38+00:00CycloneDXcyclonedx-python-lib4.0.0https://github.com/CycloneDX/cyclonedx-python-lib/actionshttps://pypi.org/project/cyclonedx-python-lib/https://cyclonedx.github.io/cyclonedx-python-lib/https://github.com/CycloneDX/cyclonedx-python-lib/issueshttps://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSEhttps://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.mdhttps://github.com/CycloneDX/cyclonedx-python-libhttps://cyclonedx.orgexpression-preferredApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE(Apache-2.0 OR MIT)
\ No newline at end of file
diff --git a/tests/fixtures/xml/1.4/regression365_multiple-licenses.xml b/tests/fixtures/xml/1.4/regression365_multiple-licenses.xml
new file mode 100644
index 000000000..0630eb215
--- /dev/null
+++ b/tests/fixtures/xml/1.4/regression365_multiple-licenses.xml
@@ -0,0 +1 @@
+2022-06-15T13:05:12+00:00CycloneDXcyclonedx-python-lib4.0.0https://github.com/CycloneDX/cyclonedx-python-lib/actionshttps://pypi.org/project/cyclonedx-python-lib/https://cyclonedx.github.io/cyclonedx-python-lib/https://github.com/CycloneDX/cyclonedx-python-lib/issueshttps://github.com/CycloneDX/cyclonedx-python-lib/blob/main/LICENSEhttps://github.com/CycloneDX/cyclonedx-python-lib/blob/main/CHANGELOG.mdhttps://github.com/CycloneDX/cyclonedx-python-libhttps://cyclonedx.orgmultiple-licensesApache-2.0VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=https://www.apache.org/licenses/LICENSE-2.0.txtOSI_APACHE
\ No newline at end of file
diff --git a/tests/test_regression365.py b/tests/test_regression365.py
new file mode 100644
index 000000000..88edb2baa
--- /dev/null
+++ b/tests/test_regression365.py
@@ -0,0 +1,105 @@
+# encoding: utf-8
+# This file is part of CycloneDX Python Lib
+#
+# 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.
+
+import re
+from datetime import datetime, timezone
+from unittest import TestCase
+from os.path import dirname, join
+from typing import Type, Union
+from itertools import product, chain
+from uuid import UUID
+
+from ddt import idata, ddt, unpack
+
+from cyclonedx.model import LicenseChoice, License, AttachedText, Encoding, XsUri
+from cyclonedx.model.bom import Bom, Component
+from cyclonedx.output.json import Json, JsonV1Dot4, JsonV1Dot3, JsonV1Dot2
+from cyclonedx.output.xml import Xml, XmlV1Dot4, XmlV1Dot3, XmlV1Dot2, XmlV1Dot1, XmlV1Dot0
+
+_bom_multiple_licenses = Bom(serial_number=UUID(hex='92f71d34625a449798913333c56a7af1'))
+_bom_multiple_licenses.metadata.timestamp = datetime(2022, 6, 15, 13, 5, 12, 0, timezone.utc)
+_bom_multiple_licenses.components.add(Component(
+ name='multiple-licenses',
+ bom_ref='testing',
+ licenses=[
+ LicenseChoice(
+ license=License(
+ id='Apache-2.0',
+ text=AttachedText(
+ content='VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=',
+ encoding=Encoding.BASE_64
+ ),
+ url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.txt')
+ )
+ ),
+ LicenseChoice(license=License(name='OSI_APACHE'))
+ ]))
+
+_bom_expression_preferred = Bom(serial_number=UUID(hex='66f6f3d40d244db3b69cbd547be9b0d3'))
+_bom_expression_preferred.metadata.timestamp = datetime(2022, 6, 15, 13, 9, 38, 0, timezone.utc)
+_bom_expression_preferred.components.add(Component(
+ name='expression-preferred',
+ bom_ref='testing',
+ licenses=[
+ LicenseChoice(
+ license=License(
+ id='Apache-2.0',
+ text=AttachedText(
+ content='VGVzdCBjb250ZW50IC0gdGhpcyBpcyBub3QgdGhlIEFwYWNoZSAyLjAgbGljZW5zZSE=',
+ encoding=Encoding.BASE_64
+ ),
+ url=XsUri('https://www.apache.org/licenses/LICENSE-2.0.txt')
+ )
+ ),
+ LicenseChoice(expression='(Apache-2.0 OR MIT)'),
+ LicenseChoice(license=License(name='OSI_APACHE'))
+ ]))
+
+
+@ddt
+class Regression365(TestCase):
+ """
+ This is a regression test against https://github.com/CycloneDX/cyclonedx-python-lib/issues/365
+
+ license list serialization must be like:
+ - if list contains any expressions: serialize the first expression only
+ - if list contains no expression: serialize all items
+ """
+
+ @idata(chain(
+ product(
+ [_bom_multiple_licenses, _bom_expression_preferred],
+ ['xml'],
+ [XmlV1Dot4, XmlV1Dot3, XmlV1Dot2, XmlV1Dot1, XmlV1Dot0]
+ ),
+ product(
+ [_bom_multiple_licenses, _bom_expression_preferred],
+ ['json'],
+ [JsonV1Dot4, JsonV1Dot3, JsonV1Dot2]
+ ),
+ ))
+ @unpack
+ def test_serialize(self, bom: Bom, target: str, schema_type: Union[Type[Json], Type[Xml]]) -> None:
+ serializer = schema_type(bom)
+ serialized = serializer.output_as_string()
+
+ expected_file = join(
+ dirname(__file__),
+ f'fixtures/{target}/{serializer.get_schema_version()}/regression365_{bom.components[0].name}.{target}')
+ with open(expected_file) as expected_fh:
+ self.assertEqual(expected_fh.read(), serialized)