jdx/mise · error

expected dictionary, got {value:?}

Error message

expected dictionary, got {value:?}

What it means

A test panic in src/system/launchd.rs: after `render_plist` produces XML, the test parses it with `Value::from_reader_xml` and asserts the top-level value is a `Value::Dictionary`. Any other plist value (array, string, etc.) reaches `value => panic!("expected dictionary, got {value:?}")`. It means the generated launchd plist is not a top-level dictionary as launchd requires.

Source

Thrown at src/system/launchd.rs:741

            nice: None,
            start_calendar_interval: Some(LaunchdCalendarIntervals::Single(
                LaunchdCalendarInterval {
                    hour: Some(2),
                    minute: Some(0),
                    ..Default::default()
                },
            )),
            queue_directories: vec!["~/Library/Queues/sync".to_string()],
            environment,
            working_directory: Some("~".to_string()),
            stdout_path: Some("~/Library/Logs/sync.log".to_string()),
            stderr_path: Some("~/Library/Logs/sync.err.log".to_string()),
            kickstart: false,
        };
        let plist = render_plist(&request).unwrap();
        let dict = match Value::from_reader_xml(Cursor::new(plist.as_slice())).unwrap() {
            Value::Dictionary(dict) => dict,
            value => panic!("expected dictionary, got {value:?}"),
        };
        assert_eq!(
            dict.get("Label"),
            Some(&Value::String("dev.mise.sync".to_string()))
        );
        assert_eq!(dict.get("RunAtLoad"), Some(&Value::Boolean(true)));
        assert_eq!(dict.get("KeepAlive"), Some(&Value::Boolean(true)));
        assert_eq!(dict.get("StartInterval"), Some(&Value::Integer(60.into())));
        assert_eq!(
            dict.get("ThrottleInterval"),
            Some(&Value::Integer(300.into()))
        );
        assert_eq!(
            dict.get("QueueDirectories"),
            Some(&Value::Array(vec![Value::String(
                crate::dirs::HOME
                    .join("Library")
                    .join("Queues")

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Print the raw plist XML from `render_plist` to inspect the root element.
  2. Verify `render_plist` always wraps keys (Label, ProgramArguments, etc.) in a top-level `<dict>`.
  3. Check the plist crate version/API (`Value::from_reader_xml`) for parsing behavior changes.
  4. Validate the rendered XML with `plutil -lint` (on macOS) to catch structural issues.

Example fix

// before
let dict = match Value::from_reader_xml(Cursor::new(plist.as_slice())).unwrap() {
    Value::Dictionary(dict) => dict,
    value => panic!("expected dictionary, got {value:?}"),
};
// after
let value = Value::from_reader_xml(Cursor::new(plist.as_slice())).unwrap();
let Value::Dictionary(dict) = value else {
    panic!("expected dictionary, got {value:?}; xml: {plst}", plst = plist)
};
Defensive patterns

Strategy: type-guard

Validate before calling

// rust
if !plist.trim_start().starts_with("<?xml") || !plist.contains("<dict>") {
    return Err("rendered plist root is not a dictionary".into());
}

Type guard

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

Try / catch

// rust
let value = Value::from_reader_xml(Cursor::new(plist.as_slice()))?;
let Value::Dictionary(dict) = value else {
    return Err(anyhow!("expected dictionary, got {value:?}; xml: {plist}"));
};

Prevention

When it happens

Trigger: `render_plist(&request).unwrap()` producing XML whose root element is not a `<dict>` — e.g. a serializer regression, empty plist output, or the request rendering path writing a scalar/array.

Common situations: Changes to the plist XML writer; malformed `LaunchdRequest` fields producing invalid XML that parses to a non-dictionary; snapshot/format changes in `render_plist`.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/95249a2d608befab. Report an issue: GitHub.