When a workflow reads an SBOM, it converts it into the
canonical format; when it writes one, it converts back. This
page documents how the fields correspond and what to expect from a round trip.
Useful when you need to explain why an exported SBOM is not byte-identical to
the one that went in, or where a particular field ended up.
The structural idea
Different formats model components differently. The canonical format reduces
them all to a flat list of elements plus a flat list of relationships, and the
converters do the reshaping.
| Source concept | Canonical representation |
|---|
SPDX packages[], files[], snippets[] | One elements[] array, tagged PACKAGE / FILE / SNIPPET |
| CycloneDX components, services, tools | elements[] tagged PACKAGE / APPLICATION / FRAMEWORK / SERVICE / TOOL |
CycloneDX metadata.component | An element, plus a DESCRIBES relationship from the document |
| CycloneDX nested components | Flattened into CONTAINS relationships |
CycloneDX dependencies[] | Flattened into DEPENDS_ON relationships |
SPDX relationships[] | Passed through nearly one to one |
| External and cross-references | Consolidated into document-level references[] |
SPDX extractedLicensingInfo, CycloneDX named licenses | customLicenses[] |
Element types
| CycloneDX type | Canonical type | SPDX equivalent |
|---|
library | PACKAGE | packages[] |
application | APPLICATION | packages[] on export |
framework, container | FRAMEWORK | packages[] on export |
file | FILE | files[] |
service | SERVICE | packages[] on export |
from metadata.tools | TOOL | creator or packages[] |
| — | SNIPPET | snippets[] |
Exporting to SPDX turns every package-like type back into packages[]; only
FILE and SNIPPET become SPDX files and snippets.
CycloneDX
Document level
| CycloneDX | Canonical | Notes |
|---|
serialNumber | id | The urn:uuid: prefix is stripped on the way in and restored on the way out. |
metadata.component.name | name | Falls back to the document id when there is no metadata component. |
version | version | |
specVersion | format | Recorded as CycloneDX-<specVersion>. |
metadata.timestamp | created | |
metadata.tools[] | creators[] and TOOL elements | Surfaced both ways. |
vulnerabilities[] | vulnerabilities[] | |
Components
| CycloneDX | Canonical element | Notes |
|---|
bom-ref | id | |
name, version, description | Same | |
type | type | library → PACKAGE, container → FRAMEWORK; anything unrecognized becomes library on export. |
purl, cpe, copyright | Same | |
hashes[] | checksums[] | Algorithm names normalized, e.g. SHA_512 → SHA-512. |
licenses[] | licensesConcluded[] | Expressions are kept as expressions; unknown names become custom licenses. |
supplier | supplier, plus a supplier participant | |
author, publisher | Author and publisher participants | |
swid | swid | |
group, scope, pedigree.notes | properties[] | Restored on export. |
externalReferences[] | Document-level references[] | Each tagged with the component it belongs to. |
SPDX
Document level
| SPDX | Canonical |
|---|
creationInfo.spdxVersion | format and version |
SPDXID | id |
name | name |
documentNamespace | namespace |
creationInfo.creators | creators[] — actor type becomes PERSON, ORG, or TOOL |
creationInfo.created | created |
dataLicense | license |
creationInfo.licenseListVersion | licenseNamespace |
externalDocumentRefs | references[] |
Packages
| SPDX | Canonical element | Notes |
|---|
SPDXID | id | Rewritten to a valid SPDXRef-… on export. |
name, versionInfo | name, version | |
filesAnalyzed | granularity | true → FILE, false → NONE. |
downloadLocation | download | |
packageFileName | file | |
supplier, originator | Same | NOASSERTION and NONE are dropped. |
checksums[] | checksums[] | |
packageVerificationCode | A synthetic checksum entry | Split back out on export. |
homepage, sourceInfo | home, origin | |
licenseConcluded | licensesConcluded[] | Joined into a single expression on export. |
licenseDeclared | licenseDeclared | |
licenseInfoFromFiles | licenseFromFiles[] | |
licenseComments | licenseComment | |
copyrightText | copyright | |
summary, description, comment | Same | |
attributionTexts | attribution[] | |
primaryPackagePurpose | purpose | SPDX 2.3 only. |
releaseDate, builtDate, validUntilDate | released, built, expiration | SPDX 2.3 only. |
Files, snippets, and the rest
| SPDX | Canonical |
|---|
File SPDXID | Element id with type FILE |
fileTypes[] | types[] |
fileContributors | contributors[] |
noticeText | notice |
Snippet snippetFromFile | file, with type SNIPPET |
| Snippet byte and line ranges | range.characters, range.lines |
relationships[] | relationships[] (from, to, type) |
annotations[] | annotations[] |
extractedLicensingInfo[] | customLicenses[] |
What to expect from a round trip
A conversion out and back preserves content faithfully, but not always
byte-for-byte. Specifically:
- Identifiers may change. Exporting to SPDX rewrites every id to a valid
SPDXRef-…. CycloneDX preserves bom-ref.
- Structure may be reshaped. Roots are inferred rather than stored, nesting
is rebuilt from relationships, and dependency references pointing at things
that are not emitted as CycloneDX components are dropped so the output stays
valid.
- Licenses are parsed, not copied. Compound and comma-separated license
names are turned into SPDX expressions, exceptions are joined with
WITH, and
anything unrecognized becomes a custom license reference.
- Targeting an older version drops newer fields. Emitting SPDX 2.2 removes
2.3-only fields and fills mandatory license and copyright fields with
NOASSERTION where they are absent.