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

Installbash
npm install @wardenkit/open-source-licence-policy
normalize.mjsjavascript
import { 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

FieldMeaning
validWhether the expression parsed and every identifier resolved against the pinned list.
normalizedExpressionThe canonical form, or null when the expression is invalid.
shapelisted-id, license-ref or complex — or null when invalid.
diagnosticsCoded, 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.

Executed outputjavascript
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

Executed output for invalid inputjavascript
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 codeCauseWhat to do
EXPR_SYNTAXThe string does not parse under the Annex B grammar.Fix the string. Mis-cased operators and stray punctuation are the common causes.
EXPR_UNKNOWN_IDThe 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_IDThe identifier after WITH is not a listed exception.Check the exception identifier's exact spelling.
EXPR_DEPRECATED_IDThe identifier resolves but is flagged deprecated.Decide deliberately. No successor identifier is ever substituted for you.
EXPR_DOCUMENTREF_UNSUPPORTEDA 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.

audit-expressions.mjsjavascript
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

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.