angular/angular-cli · error · TypeError

"${name}" must be a JSON value.

Error message

"${name}" must be a JSON value.

What it means

In ProjectDefinitionCollection.add(), any property that is not one of the known keys (root, sourceRoot, prefix, targets, etc.) is treated as an extension and must be a JSON-serializable value (checked by isJsonValue). Passing functions, undefined, symbols, or other non-JSON values in the definition object throws this TypeError.

Source

Thrown at packages/angular_devkit/core/src/workspace/definitions.ts:182

        if (target) {
          project.targets.set(name, target);
        }
      }
    }

    for (const [name, value] of Object.entries(definition)) {
      switch (name) {
        case 'name':
        case 'root':
        case 'sourceRoot':
        case 'prefix':
        case 'targets':
          break;
        default:
          if (isJsonValue(value)) {
            project.extensions[name] = value;
          } else {
            throw new TypeError(`"${name}" must be a JSON value.`);
          }
          break;
      }
    }

    super.set(definition.name, project);

    return project;
  }

  override set(name: string, value: ProjectDefinition): this {
    this._validateName(name);

    super.set(name, value);

    return this;
  }

View on GitHub (pinned to bb72145f9a)

Solutions

  1. Only include JSON-serializable values (string, number, boolean, null, plain objects, arrays) for extension properties.
  2. Move non-serializable behavior (functions, instances) out of the definition object into your own code.
  3. Validate the definition object before calling add (strip unknown non-JSON keys).

Example fix

// before
workspace.addProject({ name: 'app', root: 'apps/app', hooks: { init: fn } });
// after
workspace.addProject({ name: 'app', root: 'apps/app', hooksDescription: 'init handled externally' });
Defensive patterns

Strategy: type-guard

Validate before calling

function isJsonValue(v: unknown): boolean {
  return v === null || ['string', 'number', 'boolean'].includes(typeof v)
    || (Array.isArray(v) && v.every(isJsonValue))
    || (typeof v === 'object' && v !== null && !Array.isArray(v) && Object.values(v).every(isJsonValue));
}
// strip non-JSON extra keys before add
const extras = Object.fromEntries(Object.entries(def).filter(([, v]) => isJsonValue(v)));

Type guard

function isJsonValue(v: unknown): boolean {
  if (v === null || ['string', 'number', 'boolean'].includes(typeof v)) return true;
  if (Array.isArray(v)) return v.every(isJsonValue);
  if (typeof v === 'object' && v !== null) return Object.values(v).every(isJsonValue);
  return false;
}

Try / catch

try {
  project = workspace.addProject(def);
} catch (e) {
  if (e instanceof TypeError && /must be a JSON value/.test(e.message)) {
    const { [e.message.match(/"(.+)"/)?.[1] ?? '']: _dropped, ...rest } = def;
    project = workspace.addProject(rest);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling addProject/add with an extra property whose value is a function, class instance, undefined, symbol, or otherwise not JSON-representable, e.g. { name: 'app', root: '', onInit: () => {} }.

Common situations: Passing callbacks/objects into project definitions expecting them to be stored as extensions; leaking class instances from tooling into angular.json extensions; typos creating unexpected keys with non-JSON values.

Related errors


AI-assisted analysis of angular/angular-cli@bb72145f9a (2026-08-30). Data as JSON: /api/errors/2643c0a0f8021e8c. Report an issue: GitHub.