jdx/mise · error

expected dictionary along defaults patch path

Error message

expected dictionary along defaults patch path: {request}

What it means

When evaluating a macOS `defaults` patch, the code walks a plist path expecting every intermediate node to be a dictionary. If a component along the path resolves to a non-dictionary value (array, string, date, etc.), it cannot descend further and this error is raised, naming the full request.

Solutions

  1. Inspect the actual plist (`defaults read <domain>`) and correct the path to only traverse dictionaries
  2. Delete/reset the offending key so patch_value can create a dictionary at that position
  3. Update the patch configuration to match the current plist structure

Example fix

// before: path = ["Window", "Frame"] where 'Window' is a string
// after: path = ["NSWindow Frame"]["Main"] — fix path or reset key:
// defaults delete MyApp Window
Defensive patterns

Strategy: type-guard

Validate before calling

fn path_is_dict_traversable(root: &plist::Value, path: &[String]) -> bool {
    let mut cur = Some(root);
    for key in path {
        match cur {
            Some(plist::Value::Dictionary(d)) => cur = d.get(key),
            _ => return false,
        }
    }
    true
}

Type guard

fn as_dictionary(v: Option<&plist::Value>) -> Option<&plist::Dictionary> {
    match v { Some(plist::Value::Dictionary(d)) => Some(d), _ => None }
}

Try / catch

match selected_value(&request) {
    Err(e) if e.to_string().starts_with("expected dictionary along defaults patch path") => {
        eprintln!("plist shape mismatch for {}: reading live defaults", request);
    }
    r => r?,
}

Prevention

When it happens

Trigger: A defaults patch whose `path` traverses a key that exists but holds a scalar or array instead of a nested dictionary — e.g. path ["a","b"] where plist['a'] is a string.

Common situations: Domain plist shape changed between macOS versions, user hand-edited the plist, or the patch path was written against a different schema than the actual domain.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/6a722420d4b8656b. Report an issue: GitHub.

Appendix: source

Thrown at src/system/defaults.rs:331

            eyre::ensure!(
                !overlaps,
                "overlapping macOS defaults declarations: {other}; {request}"
            );
        }
    }
    Ok(())
}

fn selected_value<'a>(
    mut current: Option<&'a plist::Value>,
    request: &DefaultsRequest,
) -> Result<Option<&'a plist::Value>> {
    if let Some(path) = &request.path {
        for component in path {
            current = match current {
                Some(plist::Value::Dictionary(dict)) => dict.get(component),
                None => return Ok(None),
                Some(_) => eyre::bail!("expected dictionary along defaults patch path: {request}"),
            };
        }
    }
    Ok(current)
}

#[cfg(any(target_os = "macos", test))]
fn patch_value(current: &mut plist::Value, path: &[String], value: plist::Value) -> Result<()> {
    let Some((key, rest)) = path.split_first() else {
        eyre::bail!("defaults patch path must not be empty");
    };
    let dict = current
        .as_dictionary_mut()
        .ok_or_else(|| eyre::eyre!("expected dictionary along defaults patch path"))?;
    if rest.is_empty() {
        dict.insert(key.clone(), value);
    } else {
        if !dict.contains_key(key) {

View on GitHub (pinned to 533346cc37)