The SBOM ecosystem has two dominant formats, and the industry uses both. Your CI pipeline generates CycloneDX, but your government customer wants SPDX. Your vendor sends SPDX, but your vulnerability platform ingests CycloneDX. Format conversion is not a nice-to-have -- it's an operational requirement.
But conversion is not lossless. The two formats model software differently, and data can be lost, mangled, or misrepresented in translation. Understanding what converts cleanly and what doesn't is the difference between a useful SBOM and a misleading one.
The Data Model Mismatch
CycloneDX and SPDX are not just different serializations of the same data model. They have fundamentally different structures.
CycloneDX is component-centric. Everything orbits around a flat list of components with properties and relationships.
SPDX is document-centric. It models packages, files, snippets, and relationships as first-class entities with rich provenance metadata.
Fields that exist in one format but not the other:
| Feature | CycloneDX | SPDX | |---------|-----------|------| | Component scope (required/optional) | Yes | No (use relationships) | | Services | Yes | No | | Formulation (build info) | Yes | No (until 3.0) | | File-level information | Limited | Extensive | | Snippet-level licensing | No | Yes | | Package verification code | No | Yes | | License expressions | Simple | Rich (SPDX syntax) | | VEX/Vulnerability data | Native | External references (2.3) | | Composition completeness | Yes | No |
Any data in the "Yes/No" cells will be lost in conversion. There's no way around it.
Conversion Tools
CycloneDX CLI
The CycloneDX CLI includes conversion capabilities:
# Install CycloneDX CLI
npm install -g @cyclonedx/cyclonedx-cli
# Convert SPDX to CycloneDX
cyclonedx-cli convert \
--input-file sbom.spdx.json \
--input-format spdxjson \
--output-file sbom.cdx.json \
--output-format json
# Convert CycloneDX to SPDX (limited)
cyclonedx-cli convert \
--input-file sbom.cdx.json \
--input-format json \
--output-file sbom.spdx.json \
--output-format spdxjson
The CycloneDX CLI does a reasonable job but prioritizes CycloneDX fidelity. Converting to SPDX may produce documents that are technically valid but miss SPDX-specific metadata.
SPDX Tools
The official SPDX tools provide conversion utilities:
pip install spdx-tools
# Validate and convert
pyspdxtools_converter input.spdx.json output.cdx.json
protobom
protobom from the OpenSSF is a format-neutral library for SBOM manipulation:
# protobom can read and write both formats
# It uses a neutral internal representation
protobom convert --input sbom.spdx.json --output sbom.cdx.json --format cyclonedx
protobom's neutral internal model minimizes data loss by mapping fields to the closest equivalent in the target format.
sbom-convert (cdxgen ecosystem)
# Part of the cdxgen ecosystem
npx @cyclonedx/cdxgen --convert --input sbom.spdx.json --output sbom.cdx.json
What Gets Lost
SPDX to CycloneDX Losses
- File-level information -- CycloneDX doesn't model individual files the way SPDX does
- Snippet licensing -- no equivalent in CycloneDX
- Package verification codes -- SPDX-specific integrity mechanism
- Detailed license expressions -- CycloneDX supports licenses but not the full SPDX expression syntax
- Document-level relationships -- SPDX's
DESCRIBESandDESCRIBED_BYdon't map cleanly
CycloneDX to SPDX Losses
- Services -- SPDX has no service model
- Vulnerability/VEX data -- SPDX 2.3 only supports external references for security, not embedded vulnerability objects
- Composition completeness -- no SPDX equivalent
- Formulation -- no SPDX 2.3 equivalent (3.0 adds build profiles)
- Component scope -- must be inferred from relationship types
Common Pitfalls
Identifier mapping. CycloneDX uses bom-ref as internal identifiers. SPDX uses SPDXID with specific formatting rules (SPDXRef- prefix). Conversion tools need to map between these, and round-trip conversion (CycloneDX -> SPDX -> CycloneDX) will not preserve original bom-ref values.
License handling. SPDX license expressions use a specific syntax: MIT AND Apache-2.0, GPL-2.0-only OR MIT. CycloneDX supports license objects with id or name fields. Complex SPDX expressions like (MIT AND Apache-2.0) OR GPL-3.0-only may not convert cleanly.
Dependency relationships. CycloneDX uses a separate dependencies array. SPDX uses relationships with typed edges. Most tools handle DEPENDS_ON correctly, but nuanced relationship types like BUILD_TOOL_OF or OPTIONAL_DEPENDENCY_OF may be dropped.
Conversion Best Practices
1. Generate Natively When Possible
The best conversion is no conversion. If your toolchain supports both formats, generate each natively rather than converting:
# Generate both formats from the same source
syft dir:./project -o cyclonedx-json > sbom.cdx.json
syft dir:./project -o spdx-json > sbom.spdx.json
This avoids conversion losses entirely.
2. Validate After Conversion
Always validate the converted SBOM:
# Validate CycloneDX
cyclonedx-cli validate --input-file sbom.cdx.json --input-format json
# Validate SPDX
pyspdxtools_parser sbom.spdx.json
3. Check Component Counts
A quick sanity check: the converted SBOM should have the same number of packages as the source:
# Count CycloneDX components
cat sbom.cdx.json | jq '.components | length'
# Count SPDX packages
cat sbom.spdx.json | jq '.packages | length'
If the counts don't match, investigate what was dropped.
4. Preserve the Original
Always keep the original format alongside the conversion. The original contains data that the conversion lost. Store both.
5. Document the Conversion
Record which tool and version performed the conversion, and what data was lost:
{
"metadata": {
"tools": [
{
"name": "cyclonedx-cli",
"version": "0.25.0"
}
],
"properties": [
{
"name": "conversion-source",
"value": "spdx-2.3-json"
},
{
"name": "conversion-losses",
"value": "file-level-info, snippet-licensing"
}
]
}
}
CI/CD Conversion Pipeline
If you need to publish SBOMs in both formats as part of your release process:
name: Multi-Format SBOM
on:
release:
types: [published]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Generate CycloneDX SBOM
run: syft dir:. -o cyclonedx-json > sbom.cdx.json
- name: Generate SPDX SBOM
run: syft dir:. -o spdx-json > sbom.spdx.json
- name: Validate CycloneDX
run: |
npm install -g @cyclonedx/cyclonedx-cli
cyclonedx-cli validate --input-file sbom.cdx.json
- name: Validate SPDX
run: |
pip install spdx-tools
pyspdxtools_parser sbom.spdx.json
- name: Upload SBOMs
uses: actions/upload-artifact@v3
with:
name: sboms
path: |
sbom.cdx.json
sbom.spdx.json
How Safeguard.sh Helps
Safeguard accepts both CycloneDX and SPDX natively, so you can upload whatever your toolchain produces without conversion. The platform normalizes both formats into a unified internal model for vulnerability matching and policy enforcement. If you receive SBOMs from suppliers in different formats, Safeguard handles the normalization transparently -- you get a consistent view regardless of the source format. No manual conversion, no data loss from format translation.