Library / SDK
package-url/purl-spec avatar
package-url/purl-spec

PURL is a URL-shaped identifier that makes a package mean the same thing everywhere

A minimal specification for purl aka. a package "mostly universal" URL, join the discussion at https://gitter.im/package-url/Lobby

1,124 stars248 forksPythonNOASSERTION

At a glance

What is it?
The Package-URL specification defines a single string syntax, scheme, type, namespace, name, version, qualifiers and subpath, that identifies a software package independently of its ecosystem, and it is an Ecma standard as ECMA-427 with an ISO standard in process. The type-specific rules live in JSON Schema files, one per ecosystem, and the build generates and validates them.
Who is it for?
PURL fits a team that needs one identifier for a package across ecosystems, whether it is writing an SBOM, matching a dependency against a vulnerability database or linking a scanner finding back to a registry coordinate. It does not fit a tool that needs to resolve or download a package, since a PURL identifies rather than locates, and it does not fit a project that has to interoperate with a system already committed to a different coordinate format.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • package-url/purl-spec1,124 ★ · +4
Star history
Repository2026-10-192026-11-07
package-url/purl-spec1.1k1.1k

4 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Seven components, and the order matters

The whole syntax fits on one line, and the README gives it as the definition of a PURL.

text
scheme:type/namespace/name@version?qualifiers#subpath

The scheme is required and has the constant value pkg, and the README explains why that single scheme is there: one of the primary reasons is to facilitate the future official registration of the pkg scheme for Package-URLs. That is a small detail with a large implication, because a URL scheme that is not registered cannot be resolved by anything, so a PURL is deliberately a non-resolvable identifier that shares the shape of a URL.

The type is required and is the package type or protocol, such as maven, npm, nuget, gem or pypi. The namespace is optional and is a name prefix such as a Maven groupid, a Docker image owner, a GitHub user or organization, and the README notes that namespace is type-specific. The name is required and is the name of the package. The version is optional. The qualifiers are optional and carry data such as OS, architecture or repository, and they are type-specific too. The subpath is optional and identifies a location within the package, relative to the package root.

The design rule that ties it together is in the last line of that section: components are designed such that they form a hierarchy from the most significant component on the left to the least significant component on the right. So the type and namespace narrow the space, the name and version identify the thing, and the qualifiers and subpath refine it.

Each component is separated by a specific character so that parsing is unambiguous, which is the property that makes the format usable by machines rather than only by people.

The examples show why qualifiers have to be type specific

The README's list of examples is short enough to read in full, and it does more work than a rule statement would.

text
pkg:deb/debian/[email protected]?arch=i386&distro=jessie
pkg:docker/cassandra@sha256:244fd47e07d1004f0aed9c
pkg:gem/[email protected]?platform=java
pkg:golang/google.golang.org/genproto#googleapis/api/annotations
pkg:maven/org.apache.xmlgraphics/[email protected]?repository_url=repo.spring.io%2Frelease&packaging=sources
pkg:npm/%40angular/[email protected]
pkg:nuget/[email protected]
pkg:pypi/[email protected]
pkg:rpm/fedora/[email protected]?arch=i386&distro=fedora-25
pkg:rpm/opensuse/[email protected].?arch=i386&distro=opensuse-tumbleweed

Several mechanics are visible in that list. A Debian package carries the distribution and the architecture as qualifiers, and an RPM carries the same two qualifiers with different values, which shows the same idea implemented in two ecosystems. The Docker example uses a content digest as the version, and the README notes that the checksum in that example is truncated for brevity. The Go example has no version and uses a subpath, pointing at a directory inside the module, and the Maven example carries a repository URL and a packaging qualifier. The npm example has a percent-encoded scope, %40angular, because the at sign is a reserved character in a URL.

So the format does not prescribe what a qualifier means, it prescribes where a qualifier goes and that it is escaped. What the qualifier means is the type definition's business, and that is the subject of the next section.

The one thing the format does not do is tell you where to download the thing. A PURL identifies a package precisely; it does not resolve, and there is no repository host in the general case, other than as a qualifier where an ecosystem has one.

Type definitions are JSON Schema, and the index is a JSON file

The specification has two layers, and the README separates them cleanly. The core PURL syntax is defined in the Package-URL Specification, which is ECMA-427, with syntax details in Clause 5. The type-specific rules are in Clause 6, the Package-URL Type Definition Schema.

In practice, that means each package manager, platform, type or ecosystem contributes its own type definition, captured with a short string such as maven, npm and pypi. The README describes the type as the component of a Package-URL used to capture the ecosystem's own information about how to identify, locate and provision its packages.

The artefacts are all in this repository, and the README lists where.

* One JSON file for each PURL type definition, in the types/ directory. * Markdown documentation, generated from each PURL type JSON definition, in the docs/types/definitions directory. * The JSON index listing of all registered PURL types, in purl-types-index.json.

So the type definitions are machine-readable first and documentation second. That ordering is the important design decision in the whole project, because it means a new ecosystem can be added by writing a JSON file, and every tool that consumes PURLs can validate against the schema rather than against prose.

The three artefacts have distinct jobs. A type definition says how one ecosystem names, versions and qualifies its packages. The generated Markdown is the human-readable rendering of that same file. The index is the registry of which types exist, which is what a tool reads to know whether it recognises a type it has been handed.

The build generates Python models and validates the schemas

The Makefile is where the project shows that it treats the specification as something to be checked rather than described, and it does two distinct things.

The first is code generation. The CODEGEN variable runs datamodel-codegen with the JSON Schema as input and pydantic v2 models as output, with a set of options chosen to make the generated code deterministic: a target Python version of 3.10, double quotes, exact imports, standard collections, string literals wrapped, enum fields as literals, the model order kept, and the timestamp disabled. It also passes a custom file header path pointing at LICENSE, so the generated files carry the project's licence header.

The second is validation. The checkjson target runs check-jsonschema against the schemas themselves with the metaschema check enabled, then validates the index against purl-types-index.schema-1.0.json and every type definition against purl-type-definition.schema-1.1.json.

bash
${ACTIVATE} check-jsonschema --schemafile schemas/purl-type-definition.schema-1.1.json --verbose types/*-definition.json

Around that sits a small toolchain: a virtualenv target that bootstraps python3 -m venv, a conf target that installs etc/scripts/requirements.txt, formatcode running ruff with the import sort, format and lint fix passes, and formatjson running a project script over the schemas, types and tests directories.

The upshot is that a proposal to add an ecosystem is not a pull request against a markdown file, it is a pull request against a schema, and the CI either accepts it or does not. The repository root shows the same structure in prose: PURL-SPECIFICATION.rst, PURL-TYPES.rst, VERSION-RANGE-SPEC.rst and faq.md sit alongside schemas/, types/, tests/, docs/ and the etc/ directory of scripts.

Where PURL is already in use, and why that forces the design

The README lists where PURL is used as a standard identifier for software components, and the list explains the constraints the format has to satisfy.

It is used in a CycloneDX or SPDX SBOM, so it has to be something an SBOM can carry for a package from any ecosystem. It is used in most software vulnerability databases, naming OSV, the Sonatype OSS Index and VulnerableCode, which is the hard case, because those databases index findings by package identity and a finding that cannot be matched back to a package coordinate is an unusable finding. It is used in many package repositories, naming Crates.io and Packagist. And it was recently added to the standard CVE Record Format v5.2.0.

The common thread is that all of those systems are heterogeneous. An SBOM covers a build that pulls from npm, Maven, PyPI, Go modules and a dozen other places, and a vulnerability database has to answer the same question in all of them: which exact package, at which version, in which flavour. Before PURL, each ecosystem had its own coordinate syntax, and the tooling that had to work across them carried a special case for every one.

That is the challenge the README frames it against. The stated problems PURL addresses are ambiguity in package identification, cross-ecosystem interoperability, enhanced traceability and risk management, and tooling and automation, the last of which covers software composition analysis, vulnerability management and license compliance.

The claim that comes with this is that PURL embeds critical metadata directly into its structure, which is what makes it possible to compare a package across ecosystems without a lookup table, and it is why the qualifier and namespace rules have to be type specific rather than uniform.

ECMA-427 now, an ISO standard in process, and a licence to settle

PURL is an Ecma standard, ECMA-427, and it is in process to also become an ISO standard. That status is the reason to take the specification seriously as an interoperability contract, because a standard body has reviewed the document and the type definitions have a schema that implementations can validate against.

The project is organised as a specification with an implementation ecosystem around it. The README has sections for the challenges PURL addresses, why you would use it, the core syntax and the type definitions, the package-URL type definitions directory, and an Adopters section. The adopters list is maintained off the repository, on the Package-URL website, with a Specifications grid and a Tools grid, and the README asks anyone who wants to add a tool or a specification to open an issue in the website repository, noting that there are separate issue templates for Add a Tool and Add a Specification because the data fields are different.

That template split is a small but revealing detail, because it says the project treats the set of specifications and the set of tools as two different kinds of registry, with different data to capture. It also means the site, packageurl.org, is the operational centre of the project, and the README flags that the production website is at https://www.packageurl.org/ with a separate staging site mentioned alongside it.

The licence is the last loose end. The Makefile header carries SPDX-License-Identifier: MIT with Copyright (c) the purl authors and points at the project for support, and the code generation is configured to stamp the LICENSE file as the header of the generated code. The repository metadata, on the other hand, reports the licence as a custom licence that GitHub cannot classify, and there is a LICENSE file at the root. So the file at the root is the one to read, and the Makefile's MIT identifier is the best available summary of it.

On maintenance, the repository is not archived, the default branch is main, and the last push was on 2026-10-14. The recent releases are v1.1.0 on 2026-10-07, v1.0.1 on 2026-08-03 and v1.0.0 on 2025-12-19, so the 1.0 line landed in December 2025 and has been taking patch releases since, with a minor version in the mix now.

Editorial conclusion

PURL fits a team that needs one identifier for a package across ecosystems, whether it is writing an SBOM, matching a dependency against a vulnerability database or linking a scanner finding back to a registry coordinate. It does not fit a tool that needs to resolve or download a package, since a PURL identifies rather than locates, and it does not fit a project that has to interoperate with a system already committed to a different coordinate format. Before implementing it, read the type definition for the ecosystems you care about in the types/ directory, because the qualifiers and namespace rules are type specific, and check the version-range spec if your identifiers carry ranges.

Frequently asked questions

What is the structure of a Package-URL?

A PURL is composed of seven components, scheme:type/namespace/name@version?qualifiers#subpath. The scheme is required and is always pkg, the type and name are required, and the namespace, version, qualifiers and subpath are optional. Components are separated by specific characters so that parsing is unambiguous.

Where are the type specific rules for PURL kept?

In JSON Schema files, one per PURL type, in the types/ directory of the repository, with markdown documentation generated from each definition under docs/types/definitions, and a JSON index of all registered types in purl-types-index.json. The type definition schema itself is Clause 6 of ECMA-427.

Is PURL a standard, and which one?

PURL is an Ecma standard, ECMA-427, and it is in process to also become an ISO standard. The core syntax is in Clause 5 of the Package-URL Specification and the type definition schema in Clause 6.

Where is PURL used in practice?

The README lists CycloneDX and SPDX SBOMs, most software vulnerability databases such as OSV, the Sonatype OSS Index and VulnerableCode, many package repositories such as Crates.io and Packagist, and the CVE Record Format v5.2.0.

How is a change to the PURL specification checked?

The Makefile validates the JSON schemas with check-jsonschema against the metaschema, then validates the index against purl-types-index.schema-1.0.json and each type definition against purl-type-definition.schema-1.1.json, and it generates pydantic v2 models from the schemas with datamodel-codegen. The repository also holds a tests/ directory alongside schemas/ and types/.

Official sources

  1. Issues
  2. package-url/purl-spec on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/package-url-purl-spec.svg)](https://hysenlabs.com/projects/package-url-purl-spec)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/package-url-purl-spec.svg)](https://hysenlabs.com/projects/package-url-purl-spec)

Was this useful?