SPDX has had three generations. Versions 1.0 to 1.2 (2011 to 2013) described single packages. Versions 2.0 to 2.3 (2015 to 2022) added relationships, snippets and security fields, and 2.2.1 became ISO/IEC 5962:2021. SPDX 3.0 and 3.0.1 (2024) replaced the document model with elements, profiles and JSON-LD, so moving from 2.3 is a real conversion.
Below: every release with dates checked against the SPDX project's records, what the ISO edition covers, a field by field map from 2.3 to 3.0.1, the same SBOM in both versions (each validated), the official tools, what broke when we converted a real file, and a migration checklist.
Key takeaways
- Three generations, two breaks. 2.0 introduced elements and relationships; 3.0 replaced the document centred model. Within a generation, releases mostly added optional fields.
- ISO/IEC 5962:2021 is SPDX 2.2.1. Nothing from 2.3 or 3.x is in that ISO edition.
- 3.0 dropped tag-value, YAML and spreadsheets. JSON-LD, checked against a JSON schema and SHACL shapes, is the exchange format.
- Target 3.0.1, not 3.0. It fixed property and relationship names and Security profile cardinalities.
- 2.3 is still maintained. A 2.3.1 maintenance release was tagged in October 2026, and most generators still emit 2.3.
- Conversion is lossy in practice. In our test, a valid 2.3 file converted to a schema valid 3.0.1 file, but the package hash disappeared, identifiers were regenerated and a plain dependency gained a build scope.
The SPDX version timeline
Dates below come from the SPDX project's own history page on spdx.dev, the change log of the SPDX 3 model repository, and tags in the spdx-spec GitHub repository. Where those sources disagree, the table says so.
| Version | Date | What it added | Source and notes |
|---|---|---|---|
| Drafting begins | Feb 2010 | Work group under the Linux Foundation, first called "Package Facts" | spdx.dev history |
| 1.0 | Aug 2011 | First specification; describes a single package | spdx.dev history |
| 1.1 | Aug 2012 | Fixed a flaw in the package verification algorithm | spdx.dev history |
| 1.2 | Oct 2013 | Better use of the SPDX License List, more project fields | spdx.dev history |
| 2.0 | May 2015 | SPDX elements and SPDXRef identifiers, relationships, annotations, external document references, license expression syntax, multiple packages | spdx.dev history; spec Annex I |
| 2.1 | Aug 2016 | Snippets, external packages with filesAnalyzed false, ExternalRef on packages, guidance for SPDX identifiers in source files | spdx.dev history; GitHub release object created July 2017 |
| 2.1.1 | Jun 2019 | Specification moved from Google Docs to GitHub | spdx.dev history |
| 2.2 | May 2020 | JSON, YAML and development XML formats, SPDX Lite, SPDX File Tags, more relationship types, NONE and NOASSERTION in relationships | spdx.dev history; GitHub release June 2020 |
| 2.2.1 | Prepared Aug 2020, tagged Sep 2021 | No technical change; reformatted for ISO submission | spec Annex I; spdx.dev history |
| ISO/IEC 5962:2021 | Aug 2021 | International standard based on 2.2.1 | spdx.dev history; spec Annex I |
| 2.2.2 | Tagged Sep 2021, GitHub release Apr 2022 | Editorial fixes after ISO publication; JSON schema fixes | spec Annex I; spdx-spec tags |
| 2.3 | Aug 2022 | Primary package purpose, built, release and valid until dates, new hash algorithms, optional license fields, security external references, NTIA minimum element mapping | spdx.dev history; tag 11 Aug 2022 |
| 3.0 release candidate | May 2023 | Security, Build, Dataset and AI profiles previewed | spdx.dev history |
| 3.0 | 15 Apr 2024 | Element model, nine profiles, JSON-LD serialisation | spdx-3-model change log; spdx-spec tag |
| 3.0.1 | 10 Dec 2024 (model), 17 Dec 2024 (spec tag) | Name and cardinality fixes, Adler-32 restored, changes for the OMG and ISO submissions | spdx-3-model change log; spdx-spec release |
| 3.1 release candidate 1 | Jan 2026 | Published for review; not a final release | spdx-spec tag and versions list |
| 2.3.1 | Tagged 5 Oct 2026; change log lists 30 Oct 2026 | Maintenance release of 2.3: schema fixes, Annex L regulatory mapping including CISA 2026 minimum elements | spdx-spec tags and change log |
Two cautions. The GitHub "release" objects for 2.0 and 2.1 were created in July 2017, long after those versions shipped, so they are not release dates. And the 2.3.1 tag and text are live while its change log names a release date a week after this post; check the spdx-spec releases page before you pin a schema to it.
SPDX 1.0, 1.1 and 1.2: one package per document
The 1.x line was built for license compliance on a single package. If you meet a 1.x document today, treat it as a license manifest for one package: it has no relationship graph, so it cannot tell you what depends on what.
SPDX 2.0: elements and relationships
SPDX 2.0 (May 2015) is the version that made SPDX a bill of materials format. According to the differences annex in the specification, it introduced the idea of an SPDX element: documents, files and packages each get an identifier beginning with SPDXRef. Any element can then have a relationship to any other element, which is how a 2.x document expresses that a package contains files, depends on another package or was generated from source.
2.0 also added the license expression syntax (the AND, OR and WITH grammar you see in fields such as licenseDeclared), the ability to reference elements in other SPDX documents, more file types and checksum algorithms, and annotations. The old Review Information section was deprecated in favour of annotations.
SPDX 2.1: snippets and external references
SPDX 2.1 added snippets, so a portion of a file can carry different licensing from the rest of it. It let a document describe external packages without listing their files, by setting filesAnalyzed to false, and it added the ExternalRef structure on packages. ExternalRef is where package URLs (purl), CPE names and later security references live in every 2.x document you see today. 2.1 also deprecated the file level "Artifact of Project" fields and added an annex on using SPDX short identifiers in source file headers, the SPDX-License-Identifier comment that many projects now carry.
SPDX 2.2, 2.2.1 and 2.2.2: new formats and the ISO edition
SPDX 2.2 (May 2020) is where JSON, YAML and a development version of XML were added as supported file formats, next to tag-value and RDF/XML. It added SPDX Lite, a lightweight subset for scenarios where a full document is not needed, and the SPDX File Tags annex. It also widened the relationship vocabulary and allowed NONE and NOASSERTION as relationship targets to say what is unknown or known to be empty.
SPDX 2.2.1 is the same content reformatted for ISO. The specification states there were no technical differences: clauses were renumbered and annexes moved from Roman numerals to letters. 2.2.2 then fixed formatting, grammar and JSON schema typos found after ISO publication, again with no new fields.
SPDX 2.3 and 2.3.1: security and interoperability
SPDX 2.3 (August 2022) is the version most tools emit today. The specification lists its additions:
- A Primary Package Purpose field (application, library, container, operating system and others), plus Built Date, Release Date and Valid Until Date on packages.
- New hash algorithms: SHA3-256, SHA3-384, SHA3-512, BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, BLAKE3 and ADLER32.
- Several licensing fields made optional, so producers no longer have to write NOASSERTION when they have no value.
- Two relationship types, REQUIREMENT_DESCRIPTION_FOR and SPECIFICATION_FOR.
- More security external reference types (advisory, fix, url and swid) and gitoid as a persistent identifier.
- An SPDX Lite update that makes the NTIA minimum elements required, and an annex showing how those elements map to SPDX fields.
The 2.3.1 maintenance release adds no fields. Its annex says it fixes the operating system purpose enum (OPERATING-SYSTEM), states that an omitted optional field means NOASSERTION, makes the package verification code optional, requires documentNamespace in the JSON schema, adds pattern checks for identifiers, checksums and timestamps, and adds Annex L mapping the 2021 NTIA and 2026 CISA minimum elements. Expect the 2.3.1 schema to reject some files the older schema accepted.
For the regulatory side of the minimum elements, see our NTIA minimum elements field mapping.
What ISO/IEC 5962:2021 standardised, and what it did not
ISO/IEC 5962:2021 is SPDX 2.2.1. The SPDX specification describes 2.2.1 as 2.2 "reformatted for submission to ISO via the PAS process", and the project's history dates the ISO standard's availability to August 2021. The ISO catalogue page for the standard blocked our scripted fetch, so we rely on the SPDX project's own documents for its content.
What that means in practice:
- It covers the 2.2 field set: elements, relationships, snippets, ExternalRef, license expressions, SPDX Lite and File Tags.
- It does not include 2.3: no primary package purpose, release and build dates, SHA3 and BLAKE hashes or NTIA mapping annex.
- It does not include 3.x. The 3.0.1 release notes say it supports the OMG and ISO submissions, but we could not confirm on a primary source that an ISO edition of SPDX 3 has been published, so do not cite one.
So when a contract says "SPDX per ISO/IEC 5962", a 2.3 document is a superset that a 2.2 aware parser will usually read, and a 3.0.1 document is a different model entirely. Our post on whether SBOM formats are official standards covers the procurement angle.
What changed in SPDX 3.0
Our earlier pieces on what changed in SPDX 3.0 and the 3.0 feature overview cover the headline features. Here we focus on the structural changes that decide how a migration goes, as written in the specification's differences annex.
SPDX now means System Package Data Exchange
Up to 2.3, SPDX stood for Software Package Data Exchange. From 3.0 the annex says the scope has widened beyond software and SPDX now means System Package Data Exchange. That is not cosmetic: the AI and Dataset profiles describe things that are not software packages at all.
Everything is an element, and elements stand alone
In 2.3, every package, file and snippet lives inside exactly one SPDX document and inherits its namespace. In 3.0 the annex states the principle directly: every element can be expressed and referenced independently of any other element, including the document. A package can stand alone or sit inside an SpdxDocument, Sbom or Bundle, with any namespace. Consequences:
- Relationships and annotations are elements, with their own from and to, so a third party can publish a relationship or annotation about a package it did not create.
- Identifiers are full IRIs. They look like URLs, but the getting started guide is clear they are identifiers, not fetchable locations.
- External document references split in two: NamespaceMap for short prefixes, and ExternalMap for elements defined elsewhere with how to verify and locate them.
Profiles
SPDX 3.0.1's conformance clause defines nine profiles, which are compliance points: Core, Software, Security, Licensing, Dataset, AI, Build, Lite and Extension. Core is mandatory and all others are optional. Licensing comes in two forms, SimpleLicensing (a license expression as a string) and ExpandedLicensing (license expressions as structured objects). A document declares which profiles it conforms to in profileConformance on the SpdxDocument.
A tool can claim conformance to the Software profile without supporting Security or AI. That matters when you buy or build tooling: "supports SPDX 3" says nothing until you know which profiles. Our post on SPDX 3 profiles beyond software goes through the Build, Security, Licensing, Lite and Extension profiles field by field.
JSON-LD serialisation
SPDX 3 data is RDF underneath. The 3.0.1 serialisation clause says any RDF serialisation can carry SPDX data, and then defines the JSON-LD form that tools actually exchange: every document references the global context at https://spdx.org/rdf/3.0.1/spdx-context.jsonld, and spdxId and type are aliases for the JSON-LD @id and @type keywords. A document conforms when it passes two checks: structural validation against the SPDX JSON schema and semantic validation against the SPDX OWL ontology, which carries SHACL shapes.
The getting started annex adds a useful point for developers: SPDX JSON-LD is designed to be a strict subset of JSON-LD, so you can read it with a plain JSON parser if all you need is the data, or with a full JSON-LD processor if you want linked data.
The clause also defines a canonical serialisation: JSON with no line breaks, no whitespace outside strings and object keys sorted by name. It exists so that the same content always produces the same bytes, which matters when you hash or sign a document.
What 3.0.1 fixed
The spdx-3-model change log, dated 10 December 2024, lists the 3.0.1 changes. The ones that break naive tooling written against 3.0:
| Change in 3.0.1 | Before (3.0) | After (3.0.1) |
|---|---|---|
| Core property name | imports | import |
| Build property name | parameters | parameter |
| Relationship type names | hasInputs, hasOutputs | hasInput, hasOutput |
| Relationship type spelling | hasPrerequsite | hasPrerequisite |
| Licensing relationship names in profile conformance text | concludedLicense, declaredLicense | hasConcludedLicense, hasDeclaredLicense |
| Software contentType | Software profile had its own contentType | removed; File uses the Core contentType |
| VEX affected assessment | actionStatement 0..1, actionStatementTime 0..* | actionStatement 1..1, actionStatementTime 0..1 |
| Hash algorithms | no Adler-32 | adler32 restored from 2.3 |
| Individuals | none | SpdxOrganization individual and IndividualElement class added |
The spec repository's release notes add the Package URL specification and license matching guidance as annexes; our post on the 3.0.1 patch release covers the submission story.
The practical rule: emit 3.0.1. The context and schema URLs carry the version, and in our test the 3.0.1 JSON schema rejected a CreationInfo whose specVersion was "3.0". The annex also requires a full semantic version with a patch number.
Serialisations across versions
The file formats post compares formats for 2.x in depth. This table is the version view.
| Serialisation | 1.x to 2.1 | 2.2 to 2.3.1 | 3.0 and 3.0.1 |
|---|---|---|---|
| Tag-value (.spdx) | Yes | Yes | Not supported |
| RDF/XML | Yes | Yes | Allowed as an RDF serialisation by clause 6; the differences annex lists RDF/XML as not supported. Use JSON-LD for exchange |
| Spreadsheet (XLS, XLSX) | Supported by SPDX tools | Supported by SPDX tools | Not supported |
| JSON | No | Yes, with a JSON schema | Replaced by JSON-LD |
| YAML | No | Yes | Not supported |
| XML (non RDF) | No | Development version from 2.2 | Not supported |
| JSON-LD | No | No | Yes; JSON schema plus OWL and SHACL |
| Turtle, N-Triples | No | Turtle output via SPDX tools | Allowed as RDF serialisations |
| Canonical JSON | No | No | Defined for deterministic bytes |
The 3.0.1 documents disagree slightly here. Clause 6 allows any RDF serialisation, naming RDF/XML among them, while the informative differences annex lists RDF/XML as not supported. Our reading: only JSON-LD has the schema, examples and tool support, so exchange 3.x in JSON-LD. The 3.1 release candidate calls its JSON form "SPDX 3 JSON", a strict subset of JSON-LD.
Field by field: SPDX 2.3 to 3.0.1
These tables come from the "Differences between V3.0 and V2.3" section of the specification's annex, which is maintained in the spdx/using repository and linked from the 3.0.1 specification. JSON-LD names are shown as they appear in serialised documents: properties from the Core profile have no prefix, and properties from other profiles carry a prefix such as software_ or simplelicensing_.
Document and creation information
| SPDX 2.3 | SPDX 3.0.1 | Notes |
|---|---|---|
| spdxVersion "SPDX-2.3" | CreationInfo specVersion "3.0.1" | Must be full semantic version |
| SPDXID SPDXRef-DOCUMENT | SpdxDocument with an IRI spdxId | Document is now an element |
| documentNamespace | Namespace part of each spdxId; NamespaceMap for prefixes | Elements may use different namespaces |
| creationInfo.creators "Person: ..." | createdBy pointing to a Person or Organization element | createdBy is required |
| creationInfo.creators "Tool: ..." | createdUsing pointing to a Tool element | If only a tool is known, the annex says to also create a SoftwareAgent for createdBy |
| creationInfo.created | CreationInfo created | Shared CreationInfo objects are referenced by every element |
| dataLicense CC0-1.0 | dataLicense on the document | Still CC0-1.0 |
| documentDescribes, DESCRIBES | rootElement, plus a describes relationship if wanted | Sbom and SpdxDocument both have rootElement |
| externalDocumentRefs | import (ExternalMap) and namespaceMap | Document checksum becomes verifiedUsing |
| licenseListVersion "3.20" | Semantic version, for example "3.20.0" | Add a patch number |
Package
| SPDX 2.3 | SPDX 3.0.1 | Notes |
|---|---|---|
| name | name | Inherited from Element |
| versionInfo | software_packageVersion | Renamed for consistency with tag-value |
| supplier | suppliedBy (an Agent element) | Extra suppliers via availableFrom relationships |
| originator | originatedBy (Agent elements) | |
| downloadLocation | software_downloadLocation | |
| homepage | software_homePage | |
| primaryPackagePurpose | software_primaryPurpose, plus software_additionalPurpose | Now on files and snippets too |
| releaseDate, builtDate, validUntilDate | releaseTime, builtTime, validUntilTime (Core, no prefix) | Names reflect time granularity. The annex text says buildTime, but the 3.0.1 JSON schema uses builtTime |
| checksums | verifiedUsing with Hash objects; the algorithm uses HashAlgorithm values such as sha256 | "Hash" replaces "checksum" |
| packageFileName with packageChecksum | A File element linked by hasDistributionArtifact | |
| externalRefs purl | software_packageUrl, or an ExternalIdentifier | Single purl without comment goes to packageUrl |
| externalRefs cpe22Type, cpe23Type, swid | externalIdentifier | Identifiers separated from references |
| externalRefs gitoid, swh | software_contentIdentifier | |
| externalRefs advisory, fix, other security types | externalRef (ExternalRef kept) | The url type becomes securityOther |
| filesAnalyzed | Removed | The annex notes there is no longer a way to say file checksums are required |
| licenseInfoFromFiles | Removed | Suggested as an annotation on the package |
| copyrightText | software_copyrightText |
File and Snippet
| SPDX 2.3 | SPDX 3.0.1 | Notes |
|---|---|---|
| fileName | name | |
| fileTypes enum | contentType (IANA media type) plus software_primaryPurpose | SOURCE becomes purpose source; TEXT becomes text/plain |
| fileContributors | originatedBy (Person elements) | |
| checksums | verifiedUsing | |
| Snippet ranges with StartEndPointer | software_byteRange and software_lineRange as PositiveIntegerRange | Byte range now optional |
| snippetFromFile | A contains relationship from the File to the Snippet |
Relationships
| SPDX 2.3 | SPDX 3.0.1 | Swap from and to? |
|---|---|---|
| CONTAINS | contains | No |
| CONTAINED_BY | contains | Yes |
| DEPENDS_ON | dependsOn, optionally with a lifecycle scope | No |
| DEPENDENCY_OF | dependsOn | Yes |
| BUILD_DEPENDENCY_OF | dependsOn with scope build | Yes |
| DEV_DEPENDENCY_OF | dependsOn with scope development | Yes |
| TEST_DEPENDENCY_OF | dependsOn with scope test | Yes |
| RUNTIME_DEPENDENCY_OF | dependsOn with scope runtime | Yes |
| GENERATED_FROM | generates | Yes |
| DESCRIBED_BY | describes | Yes |
| PATCH_FOR, PATCH_APPLIED | patchedBy | Yes |
| STATIC_LINK | hasStaticLink | No |
| DYNAMIC_LINK | hasDynamicLink | Yes |
| Target NONE | completeness complete | |
| Target NOASSERTION | completeness noAssertion |
The swap column is where hand written converters go wrong. 3.0 relationships have a single direction, so every "_OF" type in 2.3 flips. A tool that copies spdxElementId to from without checking the table will invert half your dependency graph.
Licensing
| SPDX 2.3 | SPDX 3.0.1 | Notes |
|---|---|---|
| licenseConcluded | hasConcludedLicense relationship to a license element | Relationship names corrected in 3.0.1 |
| licenseDeclared | hasDeclaredLicense relationship | |
| License expression string | simplelicensing_LicenseExpression, or ExpandedLicensing objects | Two profiles carry the same information |
| hasExtractedLicensingInfos (LicenseRef) | CustomLicense | |
| LicenseException | ListedLicenseException (additionId, additionText, additionName) | |
| WITH operator | WithAdditionOperator (subjectLicense, subjectAddition) | Covers custom additions too |
| licenseId, licenseName, licenseComment | spdxId, name, comment | Licenses are elements now |
| LicenseInfoInFile on Package | Removed | Annotation suggested |
If your team relies on the difference between declared and concluded licenses, our declared versus concluded license guide explains why it matters. In 3.0.1 that difference survives, but it moves from two fields to two relationship types.
The same SBOM in SPDX 2.3 and SPDX 3.0.1
Here is a deliberately small SBOM: an application, example-app 1.4.0, that depends on lodash 4.17.21. Both files below are real, minimal and valid.
SPDX 2.3 JSON
This document passed the SPDX 2.3 JSON schema from the spdx-spec repository's support/2.3 branch (titled SPDX 2.3.1), the SPDX Java tools' Verify command (tools-java 2.0.7) and the SPDX Python tools (spdx-tools 0.8.3). The lodash SHA-1 value is the one the npm registry publishes for that tarball.
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "example-app-1.4.0",
"documentNamespace": "https://example.com/spdxdocs/example-app-1.4.0-6f1c2b0e-3a51-4c6e-9d0a-2b7e1f4c8a90",
"creationInfo": {
"created": "2026-10-20T09:00:00Z",
"creators": ["Organization: Example Corp", "Tool: example-sbom-builder-2.1.0"]
},
"packages": [
{
"SPDXID": "SPDXRef-Package-example-app",
"name": "example-app",
"versionInfo": "1.4.0",
"supplier": "Organization: Example Corp",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"primaryPackagePurpose": "APPLICATION",
"licenseDeclared": "Apache-2.0"
},
{
"SPDXID": "SPDXRef-Package-lodash",
"name": "lodash",
"versionInfo": "4.17.21",
"supplier": "NOASSERTION",
"downloadLocation": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"filesAnalyzed": false,
"checksums": [
{ "algorithm": "SHA1", "checksumValue": "679591c564c3bffaae8454cf0b3df370c3d6911c" }
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/lodash@4.17.21"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": "SPDXRef-Package-example-app"
},
{
"spdxElementId": "SPDXRef-Package-example-app",
"relationshipType": "DEPENDS_ON",
"relatedSpdxElement": "SPDXRef-Package-lodash"
}
]
}
SPDX 3.0.1 JSON-LD
The same content written by hand for 3.0.1. It passed structural validation against https://spdx.org/schema/3.0.1/spdx-json-schema.json, semantic validation against the SHACL shapes in https://spdx.org/rdf/3.0.1/spdx-model.ttl, and the SPDX Java tools' Verify command.
{
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
"@graph": [
{
"type": "CreationInfo",
"@id": "_:creationinfo",
"specVersion": "3.0.1",
"created": "2026-10-20T09:00:00Z",
"createdBy": ["https://example.com/spdx/agent/example-corp"]
},
{
"type": "Organization",
"spdxId": "https://example.com/spdx/agent/example-corp",
"creationInfo": "_:creationinfo",
"name": "Example Corp"
},
{
"type": "SpdxDocument",
"spdxId": "https://example.com/spdx/example-app-1.4.0/document",
"creationInfo": "_:creationinfo",
"name": "example-app-1.4.0",
"profileConformance": ["core", "software", "simpleLicensing"],
"rootElement": ["https://example.com/spdx/example-app-1.4.0/sbom"],
"element": [
"https://example.com/spdx/example-app-1.4.0/sbom",
"https://example.com/spdx/example-app-1.4.0/pkg-app",
"https://example.com/spdx/example-app-1.4.0/pkg-lodash",
"https://example.com/spdx/example-app-1.4.0/rel-depends",
"https://example.com/spdx/example-app-1.4.0/lic-mit",
"https://example.com/spdx/example-app-1.4.0/rel-lic"
]
},
{
"type": "software_Sbom",
"spdxId": "https://example.com/spdx/example-app-1.4.0/sbom",
"creationInfo": "_:creationinfo",
"software_sbomType": ["build"],
"rootElement": ["https://example.com/spdx/example-app-1.4.0/pkg-app"],
"element": [
"https://example.com/spdx/example-app-1.4.0/pkg-app",
"https://example.com/spdx/example-app-1.4.0/pkg-lodash",
"https://example.com/spdx/example-app-1.4.0/rel-depends",
"https://example.com/spdx/example-app-1.4.0/lic-mit",
"https://example.com/spdx/example-app-1.4.0/rel-lic"
]
},
{
"type": "software_Package",
"spdxId": "https://example.com/spdx/example-app-1.4.0/pkg-app",
"creationInfo": "_:creationinfo",
"name": "example-app",
"software_packageVersion": "1.4.0",
"suppliedBy": "https://example.com/spdx/agent/example-corp",
"software_primaryPurpose": "application"
},
{
"type": "software_Package",
"spdxId": "https://example.com/spdx/example-app-1.4.0/pkg-lodash",
"creationInfo": "_:creationinfo",
"name": "lodash",
"software_packageVersion": "4.17.21",
"software_packageUrl": "pkg:npm/lodash@4.17.21",
"software_downloadLocation": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"verifiedUsing": [
{
"type": "Hash",
"algorithm": "sha1",
"hashValue": "679591c564c3bffaae8454cf0b3df370c3d6911c"
}
]
},
{
"type": "LifecycleScopedRelationship",
"spdxId": "https://example.com/spdx/example-app-1.4.0/rel-depends",
"creationInfo": "_:creationinfo",
"from": "https://example.com/spdx/example-app-1.4.0/pkg-app",
"relationshipType": "dependsOn",
"to": ["https://example.com/spdx/example-app-1.4.0/pkg-lodash"],
"scope": "runtime"
},
{
"type": "simplelicensing_LicenseExpression",
"spdxId": "https://example.com/spdx/example-app-1.4.0/lic-mit",
"creationInfo": "_:creationinfo",
"simplelicensing_licenseExpression": "MIT"
},
{
"type": "Relationship",
"spdxId": "https://example.com/spdx/example-app-1.4.0/rel-lic",
"creationInfo": "_:creationinfo",
"from": "https://example.com/spdx/example-app-1.4.0/pkg-lodash",
"relationshipType": "hasDeclaredLicense",
"to": ["https://example.com/spdx/example-app-1.4.0/lic-mit"]
}
]
}
Read the two side by side and the model change is obvious. The 2.3 file nests packages and relationships inside the document. The 3.0.1 file is a flat graph: a shared CreationInfo, an Organization that both created the document and supplied the app, a document whose root is an Sbom, two packages, and three relationship and license elements that point at the packages by IRI. The 3.0.1 file is longer, and that is normal. In exchange, any element in it can be referenced from another document without copying it.
Step by step: validate and convert with the official tools
The walkthrough below uses only tools published by the SPDX project, plus general purpose JSON schema and SHACL libraries for the structural and semantic checks that the 3.0.1 specification defines. We ran every command shown here on 23 October 2026.
Step 1: find out what you have
# 2.x JSON documents carry spdxVersion at the top level
jq -r '.spdxVersion' app.spdx.json # SPDX-2.3
# 3.x JSON-LD documents carry specVersion inside CreationInfo
jq -r '."@graph"[] | select(.type=="CreationInfo") | .specVersion' app.spdx3.json # 3.0.1
# Tag-value files start with a version line
head -1 app.spdx # SPDXVersion: SPDX-2.3
If a file has spdxVersion SPDX-2.2 or earlier, the 2.3 tools read it. If it has an @context ending in /3.0/ rather than /3.0.1/, it was produced against 3.0 and needs the 3.0.1 name fixes before it will validate.
Step 2: validate 2.3 with the SPDX Python tools
The tools-python README says version 0.8 fully validates SPDX 2.2 and 2.3 documents in tag-value, RDF, JSON, YAML and XML, and converts between those formats.
python3 -m venv .venv && . .venv/bin/activate
python3 -m pip install spdx-tools
# Parse and validate; prints validation messages if the document is invalid
pyspdxtools -i app.spdx.json
# Convert 2.3 JSON to tag-value (the output format follows the file extension)
pyspdxtools -i app.spdx.json -o app.spdx
Two limits from the same README: its SPDX 3 support is experimental, "neither complete nor stable", not recommended for production, and it can only write SPDX 3, not read it. There is a bump_from_spdx2 module that converts a 2.x document into SPDX 3 elements in Python. The README describes it in terms of SPDX 3.0, so check its output against the 3.0.1 schema before trusting it.
Step 3: verify and convert with the SPDX Java tools
The tools-java README states that the tool supports SPDX 2.0, 2.1, 2.2, 2.3 and 3.0.1, converts between tag-value, RDF/XML, XLSX, XLS, JSON, XML, YAML and JSON-LD (for 3.0.1), and can verify, compare and pretty print documents. Version 2.0.7 is the current release on Maven Central.
curl -LO https://repo1.maven.org/maven2/org/spdx/tools-java/2.0.7/tools-java-2.0.7-jar-with-dependencies.jar
TOOLS="java -jar tools-java-2.0.7-jar-with-dependencies.jar"
# Verify a 2.3 document
$TOOLS Verify app.spdx.json
# This SPDX Document is valid.
# Convert 2.3 JSON to 3.0.1 JSON-LD (input and output formats given explicitly)
$TOOLS Convert app.spdx.json app.spdx3.jsonld JSON JSONLD
# Verify the 3.0.1 result
$TOOLS Verify app.spdx3.jsonld JSONLD
The README also says you can trigger the 2 to 3 conversion by naming the output with a .spdx3.json, .jsonld.json or .jsonld extension, and it points to the SPDX Online Tools at tools.spdx.org for browser based validation, comparison and conversion.
Step 4: run the two checks the 3.0.1 specification defines
Clause 6 of the 3.0.1 specification says a JSON-LD document conforms when it passes the JSON schema (structure) and the OWL ontology with SHACL shapes (semantics). This is how we checked the hand written example above:
curl -LO https://spdx.org/schema/3.0.1/spdx-json-schema.json
curl -LO https://spdx.org/rdf/3.0.1/spdx-model.ttl
curl -LO https://spdx.org/rdf/3.0.1/spdx-context.jsonld
python3 -m pip install jsonschema pyshacl
import json, jsonschema
from rdflib import Graph
from pyshacl import validate
doc = json.load(open("app.spdx3.json"))
# 1. Structural validation against the SPDX 3.0.1 JSON schema
schema = json.load(open("spdx-json-schema.json"))
errors = list(jsonschema.Draft202012Validator(schema).iter_errors(doc))
print("schema errors:", len(errors))
# 2. Semantic validation against the SHACL shapes in the SPDX model
doc["@context"] = json.load(open("spdx-context.jsonld"))["@context"] # use the local copy
data = Graph().parse(data=json.dumps(doc), format="json-ld")
shapes = Graph().parse("spdx-model.ttl", format="turtle")
conforms, _, report = validate(data, shacl_graph=shapes, ont_graph=shapes)
print("SHACL conforms:", conforms)
For our hand written 3.0.1 file, both checks passed. One caution for the SHACL step: when we ran it on the tools-java output, the shapes flagged relationships whose to field pointed at listed license IRIs such as http://spdx.org/licenses/MIT, because those license elements are not defined inside the document. The JSON schema and the SPDX Java tools' own Verify accepted the same file. Decide up front how your semantic validator should treat references to the SPDX License List, or you will chase false failures.
Step 5: put it in CI
A minimal job that fails the build when a generated SPDX file does not validate. This is a sketch for GitHub Actions; replace the generation step with whatever produces your SBOM.
name: spdx-validate
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Fetch SPDX tools
run: curl -sSLO https://repo1.maven.org/maven2/org/spdx/tools-java/2.0.7/tools-java-2.0.7-jar-with-dependencies.jar
- name: Verify SPDX 2.3 SBOM
run: java -jar tools-java-2.0.7-jar-with-dependencies.jar Verify sbom/app.spdx.json
- name: Convert to 3.0.1 and verify
run: |
java -jar tools-java-2.0.7-jar-with-dependencies.jar Convert sbom/app.spdx.json sbom/app.spdx3.jsonld JSON JSONLD
java -jar tools-java-2.0.7-jar-with-dependencies.jar Verify sbom/app.spdx3.jsonld JSONLD
Pin the tools version, as above, so a new release cannot change your output silently.
What the conversion actually did to our file
We converted the 2.3 example above with tools-java 2.0.7 and compared the result with the hand written 3.0.1 version. The output was valid against the 3.0.1 JSON schema. It was not the same SBOM.
| What we checked | 2.3 input | tools-java 3.0.1 output |
|---|---|---|
| lodash SHA-1 hash | Present in checksums | Not present anywhere in the output |
| Element identifiers | SPDXRef-Package-lodash, SPDXRef-Package-example-app | Generated IRIs ending SPDXRef-gnrtd3, SPDXRef-gnrtd5 under the namespace plus a -specv3 suffix |
| DEPENDS_ON | No lifecycle scope | LifecycleScopedRelationship dependsOn with scope build and completeness noAssertion |
| purl | ExternalRef of type purl | software_packageUrl, as the annex recommends |
| licenseConcluded on the app | Omitted | hasConcludedLicense pointing to NoAssertionLicense, consistent with the 2.3.1 rule that omission means NOASSERTION |
| Supplier "Organization: Example Corp" | Two creator and supplier strings | Three separate Organization elements all named Example Corp |
| copyrightText | Omitted | software_copyrightText set to an empty string |
| Tool creator | "Tool: example-sbom-builder-2.1.0" | A Tool element named example-sbom-builder-2.1.0 under createdUsing |
The annex leaves room for interpretation, and the scope is a tool choice, so none of this breaks the spec. It does change meaning. The lost hash weakens integrity checks, a build scope on a runtime dependency misleads anyone filtering out build only components, and regenerated identifiers break any VEX statement or ticket that referenced the old SPDXRef names. Convert, then diff the facts you care about (names, versions, purls, hashes, licenses and dependency edges), not the bytes.
Which official tools convert or validate 2.3 and 3.0.1
| Tool (SPDX project) | Reads | Writes | Validates | What it does not handle, per its README |
|---|---|---|---|---|
| tools-java 2.0.7 | 2.0 to 2.3 in tag-value, RDF/XML, JSON, XML, YAML, XLS, XLSX; 3.0.1 JSON-LD | Same formats; 2.x to 3.0.1 conversion | Verify command for 2.x and 3.0.1 | The README documents the 2 to 3 direction; it does not describe converting 3.0.1 back to 2.3 |
| tools-python (spdx-tools 0.8) | 2.2 and 2.3 in tag-value, RDF, JSON, YAML, XML | Same formats; experimental SPDX 3 writer | Full 2.2 and 2.3 validation | Cannot read SPDX 3; SPDX 3 support not production ready |
| spdx-python-model | SPDX 3 | SPDX 3 | Generated from the RDF and SHACL definitions | Low level bindings without higher level helpers |
| SPDX Online Tools (tools.spdx.org) | Uploaded documents | Converted documents | Browser based validation | Not suited to private documents you cannot upload |
| SPDX 3.0.1 JSON schema and OWL with SHACL | Any JSON-LD document | n/a | Structural and semantic conformance per clause 6 | Needs a JSON schema validator and a SHACL engine of your choice |
For conversion between SPDX and CycloneDX rather than between SPDX versions, see what gets lost converting between SPDX and CycloneDX.
Why most tools still emit 2.3, and when to switch
Our post on why your SBOM tool outputs SPDX 2.3 when 3.0 is current covers the ecosystem reasons. The short version, from what the primary sources show:
- Readers lag writers. The SPDX project's own Python library cannot read SPDX 3 yet, so consumers built on it cannot ingest 3.0.1.
- 2.3 maps to the regulations people cite. Its annexes map the NTIA minimum elements, and 2.3.1 adds the 2026 CISA minimum elements.
- 3.x is still moving. A 3.1 release candidate was published in January 2026.
That does not make 3.0.1 premature. Use this table to decide.
| Your situation | Emit | Why |
|---|---|---|
| Customers and regulators ask for "an SPDX SBOM" with minimum elements | 2.3 JSON (validate against the 2.3.1 schema) | Widest reader support; documented minimum element mapping |
| You need AI models, datasets or build provenance in the same graph | 3.0.1 JSON-LD with the AI, Dataset or Build profiles | 2.3 has no equivalent fields |
| You exchange VEX style assessments inside SPDX | 3.0.1 with the Security profile | 2.3 only links to external advisories |
| You publish elements that others reference across documents | 3.0.1 | Independent elements and ExternalMap |
| Your consumers run tools that only read 2.x | 2.3, and 3.0.1 alongside if you can | Do not force a format a reader cannot parse |
| A contract cites ISO/IEC 5962:2021 | 2.3 or 2.2.x | The ISO edition is 2.2.1; 2.3 is a compatible superset for most readers |
| You currently emit 3.0 (not 3.0.1) | 3.0.1 | Fix the renamed properties and the context URL |
Migration checklist
For producers
- Inventory what you emit today: every pipeline, version and serialisation. Tag-value and YAML have no 3.x equivalent.
- Validate 2.3 output against the 2.3.1 schema first and fix namespace, identifier and timestamp errors before converting.
- Pick the profiles you will declare. Core and Software at minimum; others only if you populate them.
- Design stable IRIs in a namespace you control, so VEX and annotations can point at them across rebuilds.
- Map identifiers deliberately: purl to software_packageUrl, CPE and SWID to externalIdentifier, gitoid to contentIdentifier.
- Flip relationship directions using the annex table, and add lifecycle scopes only where you know them.
- Keep hashes in verifiedUsing, and check the output contains them.
- Run both checks (JSON schema and SHACL) plus the SPDX Java tools' Verify.
- Publish 2.3 and 3.0.1 side by side during the transition, and diff the facts in CI.
- Pin converter and schema versions, and record them in createdUsing.
For consumers
- Detect the version before parsing. spdxVersion means 2.x; an @context with /rdf/3.0.1/ means 3.0.1.
- Route 3.0.1 to a 3.x capable reader instead of rejecting it when a 2.x parser fails.
- Normalise both into one internal model keyed on purl, name, version and hash, not SPDX identifiers.
- Resolve relationship direction from the type, not the field position.
- Treat missing as unknown. Since 2.3.1, an omitted optional field means NOASSERTION; in 3.x, completeness says whether a relationship list is complete.
- Check profileConformance before expecting Security, Build or AI data.
- Keep the original file next to anything you converted it into.
Common mistakes
- Writing specVersion "3.0" or "3.0.0" with 3.0.1 names. The context URL, schema and specVersion must all say 3.0.1.
- Copying spdxElementId into from for every relationship type. All the "_OF" relationships flip direction in 3.0.
- Assuming a converter keeps hashes, identifiers and scopes. In our test it kept none of the three as written.
- Calling 2.3 "the ISO standard". ISO/IEC 5962:2021 is 2.2.1.
- Declaring profiles you do not populate. profileConformance is a promise about content.
- Pinning to a release candidate. 3.1-RC1 is for review; declare 3.0.1 until 3.1 is final.
- Validating only the JSON schema. The spec requires the SHACL semantic check too.
- Dropping licenses in a security migration. licenseConcluded and licenseDeclared become relationships; lose them and license compliance breaks downstream.
Where Safeguard fits
Safeguard's SBOM generation and analysis is generally available. It produces SPDX in JSON, XML and RDF and CycloneDX in JSON and XML, with more than 30 export formats including CSV, Excel and PDF, and it can run on every push, on a schedule or on release. Its license compliance, also generally available, parses SPDX license expressions, applies license policy and generates NOTICE and attribution files, which is the part of an SPDX document that most migrations damage. Through generally available third party risk management you can ask vendors for their SBOMs and track supplier risk, and generally available CSAF and VEX upload lets you bring vulnerability status documents in next to them.
The honest limit: Safeguard is not an SPDX version converter. It does not translate 2.3 documents into 3.0.1 beyond its listed export formats, and this post does not claim which SPDX version each export targets; check the product documentation for that before you rely on it. For the conversion step itself, use the SPDX project's tools described above and validate their output.
FAQ
What is the latest version of SPDX?
The latest final release of SPDX 3 is 3.0.1, published in December 2024. The 2.x line also has a 2.3.1 maintenance release tagged in October 2026. A 3.1 release candidate was published in January 2026 for review and is not a final release.
Is SPDX 3.0 backward compatible with SPDX 2.3?
No. SPDX 3.0 replaced the document centred model with independent elements, turned relationships and annotations into elements, renamed many properties and dropped tag-value, YAML and spreadsheet serialisations. A 2.3 parser cannot read a 3.0.1 file, so documents must be converted.
Which SPDX version is ISO/IEC 5962:2021?
ISO/IEC 5962:2021 is SPDX 2.2.1. The SPDX specification describes 2.2.1 as 2.2 reformatted for the ISO submission with no technical changes. Fields added in 2.3 and anything from 3.x are not part of that ISO edition.
What is the difference between SPDX 3.0 and 3.0.1?
3.0.1 fixes property and relationship names that were misspelled or pluralised in 3.0 (import, parameter, hasInput, hasOutput, hasPrerequisite), corrects cardinalities in the Security profile's VEX affected assessment, removes a duplicate contentType, restores the Adler-32 hash and adds changes for the OMG and ISO submissions. Tools should emit 3.0.1.
Can SPDX 3.0 use tag-value format?
No. The specification's differences annex says tag-value, YAML, RDF/XML and spreadsheet formats are not supported in 3.0. Use the JSON-LD serialisation with the 3.0.1 context. Clause 6 allows other RDF serialisations, but JSON-LD is the one with a schema and tool support.
How do I convert SPDX 2.3 to SPDX 3.0.1?
The SPDX Java tools convert 2.x documents to 3.0.1 JSON-LD with the Convert command and the JSON and JSONLD format arguments. Validate the result against the 3.0.1 JSON schema and SHACL shapes, then compare names, versions, purls, hashes, licenses and dependency edges with the original, because conversions can change them.
Why does my SBOM tool still output SPDX 2.3?
Because most consumers still read 2.3, the SPDX project's Python library cannot read SPDX 3 yet, and 2.3 has documented mappings to the NTIA and CISA minimum elements. 2.3 is still maintained. Emit 3.0.1 when you need its profiles or independent elements, ideally alongside 2.3.
Does SPDX 3 still have license concluded and license declared?
Yes, as relationships. licenseConcluded becomes a hasConcludedLicense relationship and licenseDeclared becomes hasDeclaredLicense, each pointing to a license element. License expressions can be carried as simple strings (SimpleLicensing) or structured objects (ExpandedLicensing).
Fix it without waiting for upstream
An SPDX document, in any version, only tells you which component and version you ship; it does not ship the fixed build. Migrating formats changes how that list is written, so the remediation work that follows a finding still has to happen in your dependencies and images, and that is where the tools below apply.
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
These demos show Safeguard workflows that sit around SBOM work rather than SPDX conversion itself. To try them on your own machine, download Safeguard and read the CLI documentation.
The first demo is a real CLI session. After install, safeguard opens the interactive terminal, you ask it to fix the lodash CVE in a repository, and it reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, and pushes nothing. The same lodash entry appears in both SBOM examples in this post; an SBOM in any version tells you where it is, and this is the step that changes it.
Click through it at your own pace, or open it full screen.
The second demo sets up the Safeguard runner on your own machine, approves it with its device code and then scans from the terminal. If your SBOMs and the code they describe cannot leave your network, this is how scanning runs locally instead.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and shows how you choose which tools Claude can use. Teams that already ask an assistant questions about their projects can use the same connection to query the inventory behind their SBOMs.
Click through it at your own pace, or open it full screen.
Quick check
An SPDX 2.3 relationship says lodash DEPENDENCY_OF example-app. Which way does the 3.0.1 relationship point?
From example-app to lodash, as dependsOn. Every relationship type ending in _OF swaps direction in 3.0, according to the specification's translation table.
A contract requires SPDX "as standardised in ISO/IEC 5962:2021". Which SPDX version is that, and is a 3.0.1 document a match?
ISO/IEC 5962:2021 is SPDX 2.2.1. A 2.3 document is a near superset that most 2.2 readers accept; a 3.0.1 document uses a different model and would need agreement from the buyer.
Sources
- SPDX project overview and history (spdx.dev)
- SPDX specifications index (spdx.dev)
- SPDX specification 2.3.1, Annex I: Differences from earlier SPDX versions
- SPDX 3 differences from previous editions, spdx/using repository (linked from the 3.0.1 specification)
- SPDX specification 3.0.1, clause 5: Conformance
- SPDX specification 3.0.1, clause 6: Model and serializations
- SPDX 3 getting started guide, spdx/using repository
- SPDX 3 model change log
- spdx-spec 3.0.1 release notes
- spdx-spec releases and tags
- spdx-spec 2.3 change log (support/2.3 branch)
- SPDX specification 3.1-RC1, clause 6
- SPDX tools-python README
- SPDX tools-java README
- spdx-python-model README
- SPDX 3.0.1 JSON schema, OWL and SHACL model and JSON-LD context
- tools-java 2.0.7 on Maven Central
- ISO/IEC 5962:2021 catalogue page (iso.org; blocked our scripted fetch, content confirmed via the SPDX specification)
Last reviewed: 23 October 2026. SPDX 2.3.1 and 3.1 are in motion; check the spdx-spec releases page for the latest status.