pnpm/pnpm · error

Custom resolver returned invalid resolution: {err}

Error message

Custom resolver returned invalid resolution: {err}

What it means

The custom resolver did return a `resolution` value, but `serde_json::from_value` could not deserialize it into pnpm's Resolution type — the object's fields do not match the expected resolution shape (tarball/dir/integrity keys and their types). The appended serde error names the offending field so the mismatch is visible.

Source

Thrown at pnpm/crates/hooks/src/custom_resolver_adapter.rs:128

            let id = result.get("id").and_then(Value::as_str).ok_or_else(|| {
                let err: ResolveError = Box::new(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "Custom resolver did not return an 'id' field",
                ));
                err
            })?;

            let resolution_val = result.get("resolution").ok_or_else(|| {
                let err: ResolveError = Box::new(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "Custom resolver did not return a 'resolution' field",
                ));
                err
            })?;

            let resolution = serde_json::from_value(resolution_val.clone()).map_err(|err| {
                let resolve_err: ResolveError = Box::new(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    format!("Custom resolver returned invalid resolution: {err}"),
                ));
                resolve_err
            })?;

            // The hook's whole result is carried through, so a manifest
            // the resolver returns must survive — without it the installer
            // would re-fetch the tarball just to read `package.json`.
            let manifest = match result.get("manifest") {
                Some(manifest_val) => {
                    Some(Arc::new(serde_json::from_value(manifest_val.clone()).map_err(|err| {
                        let resolve_err: ResolveError = Box::new(std::io::Error::new(
                            std::io::ErrorKind::InvalidData,
                            format!("Custom resolver returned invalid manifest: {err}"),
                        ));
                        resolve_err
                    })?))

View on GitHub (pinned to 6261b7f388)

Solutions

  1. Read the appended serde error — it identifies the failing field and expected type
  2. Return the canonical resolution shape for the dependency type (`tarball`, `dir`, `integrity`, etc.)
  3. Cross-check the field names against a lockfile entry produced by a real dependency of the same protocol
  4. Keep `resolution` minimal — only the documented keys for that resolver type

Example fix

// before
{ id: 'myproto:foo@1.0.0', resolution: { url: 'https://example.com/foo.tgz' } }
// after
{ id: 'myproto:foo@1.0.0', resolution: { tarball: 'https://example.com/foo.tgz' } }
Defensive patterns

Strategy: validation

Validate before calling

// JS: validate the resolution shape for a tarball dependency before returning
const REQUIRED = { tarball: 'string' }
for (const [key, type] of Object.entries(REQUIRED)) {
  if (typeof result.resolution[key] !== type) {
    throw new Error(`resolution.${key} must be ${type}`)
  }
}

Type guard

/** @returns {resolution is { tarball: string }} */
function isTarballResolution(resolution) {
  return resolution != null && typeof resolution.tarball === 'string'
}

Prevention

When it happens

Trigger: `resolve()` returns `resolution` with wrong field names or types: `{ url }` instead of `{ tarball }`, strings where records are expected, or extra fields that fail deserialization for that dependency type.

Common situations: Typos in resolution keys; resolvers written against a different pnpm version's resolution schema; passing whole registry metadata as the resolution.

Related errors


AI-assisted analysis of pnpm/pnpm@6261b7f388 (2026-08-17). Data as JSON: /api/errors/346f08d0cd5f322c. Report an issue: GitHub.