serde-rs/serde · critical
MapAccess::next_value called before next_key
Error message
MapAccess::next_value called before next_key
What it means
This is a panic (via .expect at serde/src/private/de.rs:1716) inside serde's internal owned-content MapAccess, which buffers Vec<(Content, Content)> to replay map entries during #[serde(flatten)], internally/adjacently tagged enums, and Content-based deserialization. The 'value' slot (set by next_key_seed -> next_pair and taken by next_value_seed) is None, meaning next_value_seed ran without a preceding successful next_key_seed. Serde panics rather than returning an Err because the MapAccess contract is absolute: next_key must be called first and yield Some before next_value is called exactly once, and a violation is a bug in a Visitor/Deserialize impl, not malformed input.
Source
Thrown at serde/src/private/de.rs:1716
T: DeserializeSeed<'de>,
{
match self.next_pair() {
Some((key, value)) => {
self.value = Some(value);
seed.deserialize(ContentDeserializer::new(key)).map(Some)
}
None => Ok(None),
}
}
fn next_value_seed<T>(&mut self, seed: T) -> Result<T::Value, Self::Error>
where
T: DeserializeSeed<'de>,
{
let value = self.value.take();
// Panic because this indicates a bug in the program rather than an
// expected failure.
let value = value.expect("MapAccess::next_value called before next_key");
seed.deserialize(ContentDeserializer::new(value))
}
fn next_entry_seed<TK, TV>(
&mut self,
kseed: TK,
vseed: TV,
) -> Result<Option<(TK::Value, TV::Value)>, Self::Error>
where
TK: DeserializeSeed<'de>,
TV: DeserializeSeed<'de>,
{
match self.next_pair() {
Some((key, value)) => {
let key = tri!(kseed.deserialize(ContentDeserializer::new(key)));
let value = tri!(vseed.deserialize(ContentDeserializer::new(value)));
Ok(Some((key, value)))
}View on GitHub (pinned to 747814f7d5)
Solutions
- Audit every Visitor::visit_map for this type: ensure the only calls to map.next_value()/next_value_seed() occur inside a 'while let Some(key) = map.next_key()? { ... }' (or 'if let Some(key)') block, exactly one next_value per key.
- Prefer the derived Deserialize (#[derive(Deserialize)]) or next_entry/next_entry_seed over manual next_key/next_value pairing, which removes the opportunity to break the protocol.
- If you must hand-write the loop, use map.next_entry()? which yields Option<(K,V)> atomically and cannot trigger this panic.
- Temporarily remove #[serde(flatten)] or the tagged-enum attribute to isolate whether the bug is in the buffering path or in the leaf Deserialize; the panic location (owned Content variant) confirms the buffering layer is involved.
- Upgrade serde/serde_derive to the latest 1.0.x; older releases had content-buffering bugs around flatten and tagged enums that could surface this panic from otherwise-correct derived code.
Example fix
// before — broken: next_value with no preceding next_key
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where A: serde::de::MapAccess<'de>,
{
let v: i32 = map.next_value()?; // PANIC at de.rs:1716
Ok(Foo(v))
}
// after — correct pairing (or use next_entry)
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where A: serde::de::MapAccess<'de>,
{
let mut out = Foo::default();
while let Some(k) = map.next_key::<String>()? {
match k.as_str() {
"n" => out.n = map.next_value()?, // ok: key just yielded
_ => { let _: serde::de::IgnoredAny = map.next_value()?; }
}
}
Ok(out)
} Defensive patterns
Strategy: validation
Validate before calling
// Validate at authoring time: every next_value MUST be inside this loop.
// Use this idiom in every Visitor::visit_map to make the panic structurally
// impossible (no bare next_value can exist outside a successful next_key).
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where A: serde::de::MapAccess<'de>,
{
while let Some(_key) = map.next_key::<serde::de::IgnoredAny>()? {
let _val: serde::de::IgnoredAny = map.next_value()?; // ok: paired
}
Ok(Default::default())
} Try / catch
// Last-resort safety net ONLY: catch the panic at a process boundary.
// A panic from de.rs:1716 is a logic bug; do NOT use this to mask it in normal
// code paths. Fix the offending Visitor/Deserialize instead.
use std::panic::catch_unwind;
let res = catch_unwind(|| serde_json::from_str::<MyType>(input));
match res {
Ok(Ok(v)) => v,
Ok(Err(e)) => /* normal serde de error */ return Err(e.into()),
Ok(_) => unreachable!(),
Err(_panic) => /* MapAccess protocol violation in a Deserialize impl */
return Err("deserializer protocol violation: next_value before next_key".into()),
} Prevention
- Always pair map.next_key() with exactly one map.next_value() inside the same loop iteration; never call next_value() unconditionally or after next_key() returned None.
- Prefer map.next_entry() / next_entry_seed() over manual next_key + next_value — it is atomic and cannot panic.
- Prefer #[derive(Deserialize)] over hand-written Visitor for map types; only hand-write visit_map when truly necessary.
- When using #[serde(flatten)] or tagged enums, test any nested custom Deserialize with realistic inputs, since the content-buffering layer (errors at de.rs:1716/2662) is what surfaces protocol bugs there.
- Add a clippy/msrv-aware review step: grep your crate for 'next_value' and confirm each call site is lexically inside a 'while let Some(_) = .*next_key' or 'if let Some(_) = .*next_key' block.
When it happens
Trigger: A hand-written Visitor::visit_map<M: MapAccess> whose body calls map.next_value()? without first calling map.next_key()?; or calls next_value() twice for one next_key(); or ignores a None from next_key() and still calls next_value(). The panic is reached through serde's content-buffering layer, so it surfaces when the offending type sits behind #[serde(flatten)], an internally-tagged enum (#[serde(tag = "type")]), an adjacently-tagged enum, or any field whose derived Deserialize replays buffered Content via this owned MapDeserializer.
Common situations: Adding #[serde(flatten)] to a struct whose nested type has a custom Deserialize; porting a Visitor from an older serde where the key/value call order was different; a custom Deserialize for a value nested under a tagged enum whose visit_map skips the key; refactoring a visit_map loop and accidentally hoisting next_value outside the while-let-Some(key) block; a Deserialize impl that calls next_value in an else branch after next_key returned None.
Related errors
AI-assisted analysis of serde-rs/serde@747814f7d5 (2026-08-06).
Data as JSON: /data/errors/85f738aa7d0cefe1.json.
Report an issue: GitHub.