SPDX License Expression Reference: AND, OR, WITH and LicenseRef
Worked examples of SPDX license expressions: AND, OR, WITH, the + operator, parentheses and operator precedence, LicenseRef and AdditionRef custom identities, deprecated identifiers, and the canonical normalized form of each.
Task: Look up how a specific SPDX expression parses, normalizes and fails, with a runnable example for each case. · Uses @wardenkit/open-source-licence-policy · Product overview · Documentation
The model in one paragraph
An SPDX licence expression is a small formal language, not free text. It is parsed against the SPDX Specification 3.0.1 Annex B grammar, and its identifiers are resolved against a pinned SPDX License List release — 3.28.0 in this baseline. Operators are AND, OR, WITH and the trailing +; parentheses group; and two custom-identity prefixes, LicenseRef- and AdditionRef-, exist for licences and additions that are not on the list.
Every example on this page shows the canonical normalized form produced by that pinned baseline. Normalization re-emits identifiers in list case, upper-cases operators, orders the operands of the commutative AND and OR operators canonically, and drops parentheses that precedence already implies.
This page describes how expressions parse and normalize. It does not describe what any licence permits or requires: no legal meaning is inferred from any identifier, and no compatibility opinion is expressed anywhere in this product. SPDX is referenced as a technical standard family; WardenKit is not affiliated with or endorsed by SPDX or the Linux Foundation.
Single identifiers and the + operator
| Expression | Normalized | Shape | Diagnostics |
|---|---|---|---|
mit | MIT | listed-id | — |
MIT | MIT | listed-id | Surrounding whitespace is insignificant. |
GPL-2.0-or-later | GPL-2.0-or-later | listed-id | — |
LGPL-2.1 | LGPL-2.1 | listed-id | EXPR_DEPRECATED_ID (warning) |
MIT+ | MIT+ | complex | — |
GPL-2.0+ | GPL-2.0+ | complex | EXPR_DEPRECATED_ID (warning) |
Totally-Made-Up-1.0 | — | — | EXPR_UNKNOWN_ID (error) |
NOASSERTION | — | — | EXPR_UNKNOWN_ID (error) |
Two results in that table surprise people. A bare identifier with a trailing + has shape complex, not listed-id, because it denotes a family rather than one listed licence. And NOASSERTION and NONE are SPDX document field values, not licence expressions, so they do not resolve as identifiers — decide in your own inventory step what an unknown licence should become before it reaches evaluation.
Deprecated identifiers stay exactly as written and are reported, never rewritten. Choosing between GPL-2.0-only and GPL-2.0-or-later for a legacy GPL-2.0 record is a statement about intent that only you can make.
AND, OR, precedence and parentheses
AND means all of the named licences apply together. OR means a choice between them. AND binds more tightly than OR, so parentheses are only meaningful where you need to override that.
| Expression | Normalized | Shape |
|---|---|---|
MIT OR Apache-2.0 | Apache-2.0 OR MIT | complex |
apache-2.0 or mit | Apache-2.0 OR MIT | complex |
MIT AND Apache-2.0 | Apache-2.0 AND MIT | complex |
MIT AND Apache-2.0 OR BSD-3-Clause | Apache-2.0 AND MIT OR BSD-3-Clause | complex |
(MIT AND Apache-2.0) OR BSD-3-Clause | Apache-2.0 AND MIT OR BSD-3-Clause | complex |
MIT AND (Apache-2.0 OR BSD-3-Clause) | (Apache-2.0 OR BSD-3-Clause) AND MIT | complex |
Rows four and five normalize identically because the parentheses in row five only restate the default precedence. Row six keeps its parentheses because removing them would change the meaning. This is the property that makes normalized expressions safe to compare as strings, and it is why a policy rule matching an expression must be written in canonical form.
Operator case is checked rather than guessed. AND, OR and WITH may be written entirely upper-case or entirely lower-case; a mixed form is a syntax error, because silently accepting "And" would mean accepting a string that no other SPDX tool agrees is well-formed.
evaluator.normalizeExpression("MIT And Apache-2.0");
// valid: false
// EXPR_SYNTAX at 4: Operator "And" must be spelled "AND" or "and".WITH and licence exceptions
WITH attaches a listed licence exception to a licence identifier. Its right-hand side is drawn from the exceptions section of the pinned list — it is not a second licence, and it is not a free-text note.
| Expression | Normalized | Shape | Diagnostics |
|---|---|---|---|
gpl-3.0-only WITH Classpath-exception-2.0 | GPL-3.0-only WITH Classpath-exception-2.0 | complex | — |
MIT with Classpath-exception-2.0 | MIT WITH Classpath-exception-2.0 | complex | — |
Apache-2.0 WITH LLVM-exception | Apache-2.0 WITH LLVM-exception | complex | — |
GPL-2.0-only WITH Classpath-exception-2.0 OR MIT | GPL-2.0-only WITH Classpath-exception-2.0 OR MIT | complex | — |
MIT WITH Bogus-exception | — | — | EXPR_UNKNOWN_EXCEPTION_ID (error) |
MIT WITH LicenseRef-x | — | — | EXPR_SYNTAX: LicenseRef is not valid after WITH. |
A WITH expression always has shape complex, even though it names one licence — a fact worth remembering when your policy's complexExpressionTreatment decides what happens to anything you did not match explicitly. If your components legitimately carry exceptions, write an exact-normalized-expression rule for each accepted combination rather than relying on the fallback.
LicenseRef and AdditionRef custom identities
Not every licence is on the list. LicenseRef- names a custom licence identity, and AdditionRef- names a custom addition used in the exception position after WITH. Both are opaque customer-defined references: they are validated structurally, normalized, and carried through to your policy, and no legal meaning whatsoever is assigned to them.
| Expression | Normalized | Shape | Note |
|---|---|---|---|
LicenseRef-Acme-Internal | LicenseRef-acme-internal | license-ref | References normalize to lower case. |
LicenseRef-acme AND MIT | LicenseRef-acme AND MIT | complex | Composes with the ordinary operators. |
MIT WITH AdditionRef-acme | MIT WITH AdditionRef-acme | complex | AdditionRef is valid only in the addition position. |
AdditionRef-acme-extra | — | — | EXPR_SYNTAX: AdditionRef is only valid after WITH. |
DocumentRef-spdx-tool:LicenseRef-x | — | — | EXPR_DOCUMENTREF_UNSUPPORTED. |
The lower-casing matters in practice: a policy rule of type exact-license-ref must name LicenseRef-acme-internal, because a rule written as LicenseRef-Acme-Internal would match nothing. DocumentRef-qualified references are rejected outright rather than partially interpreted, because this version ingests no SPDX document context in which such a reference could be resolved.
Checking an expression yourself
Every row above is reproducible in one command against the same pinned baseline, with no account, service or network involved.
node -e "import('@wardenkit/open-source-licence-policy').then(m =>
console.log(m.createEvaluator().normalizeExpression(process.argv[1])))" "MIT AND (Apache-2.0 OR BSD-3-Clause)"npx wardenkit-licence-policy reference --json
# {
# "referenceKind": "bundled-snapshot",
# "referenceVersion": "spdx-license-list-3.28.0/snapshot-1",
# "spdxLicenseListVersion": "3.28.0",
# "spdxSpecVersion": "3.0.1",
# "spdxExpressionGrammar": "annex-b"
# }Pinning is the point. An identifier's validity and deprecation status are properties of a specific SPDX License List release, so a result is only reproducible if the release that decided it is recorded — which is why every evaluation result carries that reference version in its provenance block.
Run this locally
- Try the same workflow locally — Open-Source Licence Policy SDK quickstart
- Learn what the SDK does, its boundaries and its plans — Open-Source Licence Policy SDK
Related guides
- Validate SPDX License Expressions in Node.js — Call a local SPDX expression validator from your own Node.js code and act on its diagnostics.
- Open Source License Policy as Code — Express an existing open source licence policy as a versioned JSON document, validate it, and make its decisions reproducible.
- Run License Policy Checks in CI — Fail a build when a dependency's licence is denied by your policy, and keep the JSON evidence for the release.
Deterministic tooling, professional judgment
WardenKit performs deterministic technical validation, normalization and evaluation of the policy you author, over the records you supply. It does not provide legal advice, determine legal permissions or obligations, opine on licence compatibility, or certify compliance. Which licences your organization allows, denies or reviews remains your decision.
Product overview and pricing · All guides · Docs · Support
Corrections and questions: support@wardenkit.com.