Validate SPDX License Expressions in Node.js
Validate and normalize SPDX license expressions from Node.js: the normalizeExpression API, canonical form, expression shapes, deprecated identifiers, LicenseRef handling, diagnostic codes, and how to clean up a metadata field in bulk.
Task: Call a local SPDX expression validator from your own Node.js code and act on its diagnostics. · Uses @wardenkit/open-source-licence-policy · Product overview · Documentation
What "valid" means for an SPDX expression
An SPDX licence expression string can be wrong in three independent ways, and a validator that conflates them is hard to act on. It can fail to parse under the grammar; it can parse but name an identifier that is not in the licence list; or it can parse and resolve while using an identifier the list marks deprecated. The first is a syntax defect, the second is usually stale or invented metadata, and the third is valid but worth surfacing.
This validator separates all three. Expressions are parsed against the SPDX Specification 3.0.1 Annex B grammar and resolved against a pinned SPDX License List 3.28.0 baseline, so the answer for a given input does not drift under you between runs.
Validation is a technical operation on a string. It says nothing about what a licence permits or requires, and no legal meaning is inferred from any SPDX metadata field. SPDX is referenced as a technical standard; WardenKit is not affiliated with or endorsed by SPDX or the Linux Foundation.
Install and validate one expression
npm install @wardenkit/open-source-licence-policyimport { createEvaluator } from "@wardenkit/open-source-licence-policy";
// No arguments: uses the bundled SPDX evaluation reference snapshot. No
// entitlement, no account and no network request are involved.
const evaluator = createEvaluator();
const result = evaluator.normalizeExpression("apache-2.0 or mit");
// {
// valid: true,
// normalizedExpression: 'Apache-2.0 OR MIT',
// shape: 'complex',
// diagnostics: []
// }
if (!result.valid) {
for (const diagnostic of result.diagnostics) {
console.error(`${diagnostic.code} at ${diagnostic.position}: ${diagnostic.message}`);
}
}The evaluator is a plain local object. Constructing it reads bundled data from disk once; normalizeExpression() itself is pure, synchronous and makes no I/O call, so it is safe to call in a tight loop over thousands of strings.
Reading the result: valid, normalizedExpression, shape, diagnostics
| Field | Meaning |
|---|---|
valid | Whether the expression parsed and every identifier resolved against the pinned list. |
normalizedExpression | The canonical form, or null when the expression is invalid. |
shape | listed-id, license-ref or complex — or null when invalid. |
diagnostics | Coded, ordered diagnostics. Present on valid results too, for example a deprecation warning. |
Canonicalization is what makes the output comparable. Identifiers are re-emitted in the licence list's own case, operators are upper-cased, and because AND and OR are commutative their operands are ordered canonically — so two teams who wrote the same licence choice differently end up with the same string.
evaluator.normalizeExpression("mit");
// { valid: true, normalizedExpression: 'MIT', shape: 'listed-id', diagnostics: [] }
evaluator.normalizeExpression("MIT OR Apache-2.0");
// { valid: true, normalizedExpression: 'Apache-2.0 OR MIT', shape: 'complex', diagnostics: [] }
evaluator.normalizeExpression("MIT AND (Apache-2.0 OR BSD-3-Clause)");
// { valid: true, normalizedExpression: '(Apache-2.0 OR BSD-3-Clause) AND MIT', shape: 'complex', diagnostics: [] }
evaluator.normalizeExpression("gpl-3.0-only WITH Classpath-exception-2.0");
// { valid: true, normalizedExpression: 'GPL-3.0-only WITH Classpath-exception-2.0', shape: 'complex', diagnostics: [] }
evaluator.normalizeExpression("LicenseRef-Acme-Internal");
// { valid: true, normalizedExpression: 'LicenseRef-acme-internal', shape: 'license-ref', diagnostics: [] }Note the last two. A WITH exception makes the shape complex even though it names a single licence, and a custom LicenseRef- identity is lower-cased. Both matter when you later write policy rules against these values.
Handling the failure modes
evaluator.normalizeExpression("MIT And Apache-2.0");
// valid: false
// EXPR_SYNTAX at 4: Operator "And" must be spelled "AND" or "and".
evaluator.normalizeExpression("Totally-Made-Up-1.0");
// valid: false
// EXPR_UNKNOWN_ID: Unknown SPDX licence identifier "Totally-Made-Up-1.0".
evaluator.normalizeExpression("MIT WITH Bogus-exception");
// valid: false
// EXPR_UNKNOWN_EXCEPTION_ID: unknown exception identifier after WITH.
evaluator.normalizeExpression("DocumentRef-spdx-tool:LicenseRef-x");
// valid: false
// EXPR_DOCUMENTREF_UNSUPPORTED: DocumentRef-qualified references are not supported.
evaluator.normalizeExpression("AdditionRef-acme-extra");
// valid: false
// EXPR_SYNTAX at 0: AdditionRef is only valid after WITH.
evaluator.normalizeExpression("GPL-2.0+");
// valid: true, normalizedExpression: 'GPL-2.0+', shape: 'complex'
// EXPR_DEPRECATED_ID (warning): SPDX licence identifier "GPL-2.0" is deprecated.| Diagnostic code | Cause | What to do |
|---|---|---|
EXPR_SYNTAX | The string does not parse under the Annex B grammar. | Fix the string. Mis-cased operators and stray punctuation are the common causes. |
EXPR_UNKNOWN_ID | The identifier is not in the pinned licence list. | Correct the metadata, or express the licence as a LicenseRef- identity if it genuinely is not a listed licence. |
EXPR_UNKNOWN_EXCEPTION_ID | The identifier after WITH is not a listed exception. | Check the exception identifier's exact spelling. |
EXPR_DEPRECATED_ID | The identifier resolves but is flagged deprecated. | Decide deliberately. No successor identifier is ever substituted for you. |
EXPR_DOCUMENTREF_UNSUPPORTED | A DocumentRef-qualified reference was used. | Resolve the reference in your own tooling first; this version ingests no SPDX document-reference context. |
Deprecated identifiers are deliberately preserved exactly as pinned. Substituting GPL-2.0-only or GPL-2.0-or-later for a bare GPL-2.0 would be a judgment about intent, and that judgment is yours, not the tool's.
Cleaning up a metadata field in bulk
The most common real task is not validating one string but auditing a whole inventory column. Because the call is synchronous and local, a report over an entire component list is a few lines.
import { readFile } from "node:fs/promises";
import { createEvaluator } from "@wardenkit/open-source-licence-policy";
const evaluator = createEvaluator();
const components = JSON.parse(await readFile("components.json", "utf8"));
const invalid = [];
const deprecated = [];
const rewritten = [];
for (const component of components) {
const raw = component.licenseExpression;
const result = evaluator.normalizeExpression(raw);
if (!result.valid) {
invalid.push({ id: component.id, raw, codes: result.diagnostics.map((d) => d.code) });
continue;
}
if (result.diagnostics.some((d) => d.code === "EXPR_DEPRECATED_ID")) {
deprecated.push({ id: component.id, raw });
}
if (result.normalizedExpression !== raw) {
rewritten.push({ id: component.id, from: raw, to: result.normalizedExpression });
}
}
console.log(`invalid: ${invalid.length}, deprecated: ${deprecated.length}, non-canonical: ${rewritten.length}`);
console.table(invalid);
console.table(rewritten);Fix the invalid rows first: an unparseable or unknown expression is the one case a licence policy can never evaluate meaningfully, and a well-built policy treats it as blocking rather than passing it through. Then normalize the non-canonical rows, so that rules and reports compare equal strings.
Next: use the same validator to enforce your own policy over these components, and read the operator-by-operator reference for AND, OR, WITH and LicenseRef.
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
- SPDX License Expression Reference: AND, OR, WITH and LicenseRef — Look up how a specific SPDX expression parses, normalizes and fails, with a runnable example for each case.
- 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.