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

  1. 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.
  2. 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.
  3. If you must hand-write the loop, use map.next_entry()? which yields Option<(K,V)> atomically and cannot trigger this panic.
  4. 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.
  5. 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

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.