An SPDX relationship is a three part statement: element A, a relationship type, element B. DESCRIBES links the document to the main package, CONTAINS says A holds B inside it, and DEPENDS_ON says A needs B to build and run. Read them left to right, and watch the reverse forms.
The basic shape
In tag-value form, a relationship looks like this:
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-Package-app
Relationship: SPDXRef-Package-app DEPENDS_ON SPDXRef-Package-lib
In JSON, the same data appears in a relationships list with spdxElementId, relationshipType, and relatedSpdxElement. The first and last fields are the two ends. The middle one is the verb.
The ones you will see most
The SPDX 2.3.1 specification defines the relationship types. These matter most when reading someone else's file:
| Type | Meaning from the spec |
|---|---|
| DESCRIBES | The document describes the package. It is a logical relationship, and mandatory if more than one package or set of files is present |
| CONTAINS | A contains B, for example an archive containing a source file |
| DEPENDS_ON | A depends on the presence of B in order to build and run |
| DEPENDENCY_OF | The reverse: A is a dependency of B |
| BUILD_DEPENDENCY_OF | A is needed to build B |
| DEV_DEPENDENCY_OF | A is a development dependency of B |
| TEST_DEPENDENCY_OF | A is needed for B's tests |
| RUNTIME_DEPENDENCY_OF | A is a runtime dependency of B |
Other types exist for optional and provided dependencies, and for links such as static linking.
The direction trap
Types come in pairs. DEPENDS_ON and DEPENDENCY_OF describe the same fact from opposite ends. If a file says lib DEPENDENCY_OF app, that means app depends on lib. Read it as "lib is a dependency of app."
A quick habit: say the sentence out loud, with the verb in the middle. If it reads naturally, you have the direction right.
How to find the top of the graph
Start at the DESCRIBES relationship. It points to the package the SBOM is about. From there, follow DEPENDS_ON edges to direct dependencies, then repeat. That is your dependency tree.
Be careful with CONTAINS. It describes physical content, such as files inside an archive. It is not the same as a dependency, though for a bundled library the two can overlap.
What the types tell you about risk
Build, dev, and test dependency types are useful for scoping. A test dependency flagged with a vulnerability is rarely in the shipped product. A RUNTIME_DEPENDENCY_OF edge is. But a generator may use the plain DEPENDS_ON for everything, so do not assume the narrow types are present. If every edge is DEPENDS_ON, you cannot separate test from runtime from the file alone.
Gaps to look for
- Orphans. Packages that appear in the list but in no relationship. That could mean the generator found them but could not place them.
- Flat graphs. Every package attached directly to the root, with no deeper edges. That suggests transitive data was not captured. The NTIA minimum elements report says the full graph should be stated or the gaps declared as known unknowns.
- Missing DESCRIBES. Without it, the top of the graph is unclear.
A jq filter to list all dependency edges from an SPDX JSON file:
jq '.relationships[] | select(.relationshipType=="DEPENDS_ON")' sbom.json
Where Safeguard fits
Safeguard provides SBOM and dependency scanning, so you can compare a supplier's graph with one built from your own scan. A free scan is at gold.safeguard.sh/scan.
Quick check
Does CONTAINS mean the same as DEPENDS_ON?
No. CONTAINS is about what is physically inside something. DEPENDS_ON is about what must be present to build and run.
Why do I see a DESCRIBES line at the top?
It marks the package the document is about. It is the entry point for reading the graph.
Sources
Last reviewed: 19 October 2026.