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
- Print the raw plist XML from `render_plist` to inspect the root element.
- Verify `render_plist` always wraps keys (Label, ProgramArguments, etc.) in a top-level `<dict>`.
- Check the plist crate version/API (`Value::from_reader_xml`) for parsing behavior changes.
- 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
- Lint generated plists (plutil -lint) in CI on macOS runners.
- Always emit a top-level <dict> for launchd plists.
- Snapshot render_plist output to catch format regressions.
- Include raw output in error messages when parsing fails structurally.
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
- expected StartCalendarInterval dictionary, got {value:?}
- expected ProgramArguments array, got {value:?}
- expected EnvironmentVariables dictionary, got {value:?}
- expected first calendar interval dictionary, got {value:?}
- expected second calendar interval dictionary, got {value:?}
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/95249a2d608befab.
Report an issue: GitHub.