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

ExpressionNormalizedShapeDiagnostics
mitMITlisted-id—
MIT MITlisted-idSurrounding whitespace is insignificant.
GPL-2.0-or-laterGPL-2.0-or-laterlisted-id—
LGPL-2.1LGPL-2.1listed-idEXPR_DEPRECATED_ID (warning)
MIT+MIT+complex—
GPL-2.0+GPL-2.0+complexEXPR_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.

ExpressionNormalizedShape
MIT OR Apache-2.0Apache-2.0 OR MITcomplex
apache-2.0 or mitApache-2.0 OR MITcomplex
MIT AND Apache-2.0Apache-2.0 AND MITcomplex
MIT AND Apache-2.0 OR BSD-3-ClauseApache-2.0 AND MIT OR BSD-3-Clausecomplex
(MIT AND Apache-2.0) OR BSD-3-ClauseApache-2.0 AND MIT OR BSD-3-Clausecomplex
MIT AND (Apache-2.0 OR BSD-3-Clause)(Apache-2.0 OR BSD-3-Clause) AND MITcomplex

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.

Mis-cased operatorjavascript
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.

ExpressionNormalizedShapeDiagnostics
gpl-3.0-only WITH Classpath-exception-2.0GPL-3.0-only WITH Classpath-exception-2.0complex—
MIT with Classpath-exception-2.0MIT WITH Classpath-exception-2.0complex—
Apache-2.0 WITH LLVM-exceptionApache-2.0 WITH LLVM-exceptioncomplex—
GPL-2.0-only WITH Classpath-exception-2.0 OR MITGPL-2.0-only WITH Classpath-exception-2.0 OR MITcomplex—
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.

ExpressionNormalizedShapeNote
LicenseRef-Acme-InternalLicenseRef-acme-internallicense-refReferences normalize to lower case.
LicenseRef-acme AND MITLicenseRef-acme AND MITcomplexComposes with the ordinary operators.
MIT WITH AdditionRef-acmeMIT WITH AdditionRef-acmecomplexAdditionRef 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.

One-off checkbash
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)"
Which SPDX baseline am I resolving against?bash
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

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.