libnyanpasu/clash-nyanpasu · error
legacy verge snapshot must serialize as a mapping
Error message
legacy verge snapshot must serialize as a mapping
What it means
Raised by `legacy_patch_between` when serializing an `IVerge` snapshot with serde_yaml does not produce a YAML mapping. The function diffs `previous` vs `desired` snapshots by comparing mapping entries, so it requires both to serialize as `serde_yaml::Value::Mapping`. A non-mapping result is an internal invariant violation (e.g. an `IVerge` that serializes to a scalar/sequence, or a serialization produced `Value::Null`).
Source
Thrown at backend/tauri/src/bridge/verge.rs:461
) -> anyhow::Result<crate::state::TypedConfigPatchPlan> {
super::typed_patches_from_legacy_patch(base, patch, legacy_clash)
}
pub(crate) fn route_patch(patch: &IVerge) -> LegacyVergePatchRoute {
route_verge_patch(patch)
}
pub(crate) fn validate_patch(patch: &IVerge) -> anyhow::Result<()> {
validate_verge_patch(patch)
}
}
fn legacy_patch_between(previous: &IVerge, desired: &IVerge) -> anyhow::Result<IVerge> {
let previous = serde_yaml::to_value(previous)?;
let desired = serde_yaml::to_value(desired)?;
let previous = previous
.as_mapping()
.ok_or_else(|| anyhow::anyhow!("legacy verge snapshot must serialize as a mapping"))?;
let mut patch = desired
.as_mapping()
.ok_or_else(|| anyhow::anyhow!("legacy verge snapshot must serialize as a mapping"))?
.clone();
patch.retain(|key, value| previous.get(key) != Some(value));
Ok(serde_yaml::from_value(serde_yaml::Value::Mapping(patch))?)
}
/// Pure classifier (infallible). Validation is delegated to `validate_verge_patch`
/// or to `feat::patch_verge`. The side-effect field set mirrors `feat::patch_verge`.
#[allow(deprecated)]
fn route_verge_patch(patch: &IVerge) -> LegacyVergePatchRoute {
let legacy = patch.enable_service_mode.is_some()
|| patch.enable_tun_mode.is_some()
|| patch.enable_auto_launch.is_some()
|| patch.enable_system_proxy.is_some()
|| patch.system_proxy_bypass.is_some()
|| patch.enable_proxy_guard.is_some()View on GitHub (pinned to f7dbce2997)
Solutions
- Inspect the verge snapshot file on disk; if it is empty or not a YAML object, restore a valid mapping-shaped config.
- Verify `IVerge` still derives a struct/map-style Serialize impl; undo changes that make it serialize to a scalar, sequence, or null.
- Guard the snapshot loader to reject/replace non-mapping documents with a default `IVerge` before they reach `legacy_patch_between`.
- Add a debug assertion or unit test asserting `serde_yaml::to_value(IVerge::default()).is_mapping()`.
Example fix
// before: trusting the snapshot blindly
let previous = self.legacy_store.snapshot()?;
// after: validate shape before diffing
let previous = self.legacy_store.snapshot()?;
if serde_yaml::to_value(&previous)?.as_mapping().is_none() {
return Err(anyhow::anyhow!("verge snapshot is not a YAML mapping; reset to defaults"));
} Defensive patterns
Strategy: validation
Validate before calling
fn ensure_yaml_mapping<T: serde::Serialize>(v: &T) -> anyhow::Result<()> {
let val = serde_yaml::to_value(v)?;
anyhow::ensure!(val.is_mapping(), "expected a YAML mapping, got: {:?}", val);
Ok(())
}
// call before diffing
ensure_yaml_mapping(&previous)?;
ensure_yaml_mapping(&desired)?; Type guard
fn is_yaml_mapping(v: &serde_yaml::Value) -> bool {
matches!(v, serde_yaml::Value::Mapping(_))
} Try / catch
match legacy_patch_between(&previous, &desired) {
Err(e) if e.to_string().contains("must serialize as a mapping") => {
// reset to defaults and retry once
let defaults = IVerge::default();
legacy_patch_between(&defaults, &desired)
}
other => other,
} Prevention
- Keep `IVerge` as a struct/map-style serde representation; cover it with a serialization-shape unit test.
- Reject non-mapping (e.g. empty) verge YAML files at load time and fall back to defaults.
- Add `debug_assert!(serde_yaml::to_value(&iv).unwrap().is_mapping())` in snapshot code.
- Validate user-editable config files after manual edits or migrations.
When it happens
Trigger: Calling `run_legacy_verge_mutation` (via `patch_verge_config`) where `serde_yaml::to_value(previous)` or `serde_yaml::to_value(desired)` yields a non-mapping value — practically only when `IVerge`'s Serialize impl is changed to a non-struct representation, or the snapshot deserialized into a value that serializes as null/scalar.
Common situations: After a refactor changing `IVerge` serde representation (e.g. `#[serde(transparent)]` over a scalar or a newtype over a sequence); snapshots loaded from a config file whose top level is not a mapping (e.g. an empty file becoming `Value::Null`).
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- failed to parse config: {e}
- failed to parse profiles: {e}
- uid-derived path is always a valid managed path
- replacement target remains in the candidate snapshot
- unimplemented!()
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/2dee9fb24bfbe03a.
Report an issue: GitHub.