serde-rs/json · error
number value was not emitted
Error message
number value was not emitted
What it means
With the arbitrary_precision feature, serde_json::Number serializes itself via a private struct protocol: Number::serialize (src/number.rs:392) calls serialize_struct("$serde_json::private::Number", 1) then serialize_field("$serde_json::private::Number", &self.n). The value Serializer routes that struct name to SerializeMap::Number { out_value: None } (src/value/ser.rs:274), and serialize_field sets out_value = Some(...). The panic at src/value/ser.rs:686 fires in end() when out_value is still None — i.e. the required single field was never emitted. This is an internal invariant guarding serde_json's own Number round-trip; the constant TOKEN is defined at src/number.rs:18.
Source
Thrown at src/value/ser.rs:686
}
#[cfg(feature = "raw_value")]
SerializeMap::RawValue { out_value } => {
if key == crate::raw::TOKEN {
*out_value = Some(tri!(value.serialize(RawValueEmitter)));
Ok(())
} else {
Err(invalid_raw_value())
}
}
}
}
fn end(self) -> Result<Value> {
match self {
SerializeMap::Map { .. } => serde::ser::SerializeMap::end(self),
#[cfg(feature = "arbitrary_precision")]
SerializeMap::Number { out_value, .. } => {
Ok(out_value.expect("number value was not emitted"))
}
#[cfg(feature = "raw_value")]
SerializeMap::RawValue { out_value, .. } => {
Ok(out_value.expect("raw value was not emitted"))
}
}
}
}
impl serde::ser::SerializeStructVariant for SerializeStructVariant {
type Ok = Value;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
self.map.insert(String::from(key), tri!(to_value(value)));View on GitHub (pinned to afdf6fc672)
Solutions
- Stop reconstructing the private $serde_json::private::Number protocol; use the public serde_json::Number API (Number::from_str / from_i64 / from_f64 / serialize) so the field is always emitted correctly.
- If you are writing a Serializer that must honor the protocol, ensure serialize_struct with that name is always followed by exactly one serialize_field with the same key before end().
- Remove any hardcoded copy of the TOKEN string; if you must reference it, keep it in lockstep with the serde_json version you depend on, or avoid the protocol entirely.
- Reproduce in isolation with serde_json::to_value on your type (arbitrary_precision enabled) to confirm the field path is taken, then drop the manual mirror.
- Pin and audit serde_json versions across your workspace so the TOKEN contract does not silently drift.
Example fix
// before — reconstructing serde_json's private Number protocol
let name = "$serde_json::private::Number";
let mut s = serializer.serialize_struct(name, 1)?;
// forgot the field, or used a wrong key -> end() panics: number value was not emitted
s.end()
// after — use the public API; Number emits the field itself
let n: serde_json::Number = serde_json::Number::from_str("123456789012345678901234567890")
.map_err(serde::ser::Error::custom)?;
n.serialize(serializer) Defensive patterns
Strategy: validation
Validate before calling
// No pre-call runtime check prevents this; it fires inside end() for the
// private $serde_json::private::Number struct protocol. The only safe input
// is a real serde_json::Number. If you must accept serialized numbers, build
// them through the public API:
fn make_number(s: &str) -> Result<serde_json::Number, serde_json::Error> {
// validates the representation used by arbitrary_precision round-trips
serde_json::Number::from_str(s).ok_or_else(|| {
serde::de::Error::custom("invalid number literal")
})
} Type guard
// Not applicable: the panic is an internal-contract violation keyed on a // private struct name, not a type you can narrow at the call site.
Try / catch
// Fix the producer; do not catch. If isolating an untrusted serializer:
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
serde_json::to_value(&value_with_custom_serialize)
}));
if res.is_err() {
// number value was not emitted — stop reconstructing the TOKEN protocol
} Prevention
- Never hardcode "$serde_json::private::Number" as a struct name; use serde_json::Number.
- If you implement a Serializer honoring the protocol, always emit the field before end().
- Keep serde_json version pinned and audited so the TOKEN contract cannot drift.
- Add a test that round-trips Number through to_value with arbitrary_precision enabled.
- Search your dependency tree for copies of the private TOKEN string.
When it happens
Trigger: Code that manually calls serializer.serialize_struct("$serde_json::private::Number", 1) (copying serde_json's private TOKEN) against the to_value / value::Serializer and then calls end() without a matching serialize_field("$serde_json::private::Number", ...), or with the field key misspelled (the equality check is at src/value/ser.rs:662). A generic Serialize wrapper/adapter that forwards the struct name but drops or renames the field. A serde version skew where a downstream crate hardcodes an old/new TOKEN string that no longer matches crate::number::TOKEN. Not reachable through normal public API usage (to_value, to_string, derived Serialize) because Number::serialize always emits the field.
Common situations: A crate that tries to interoperate with serde_json's arbitrary_precision representation by reconstructing the magic struct instead of using serde_json::Number. Copy-pasting the private constant $serde_json::private::Number from serde_json source or docs. Upgrading serde_json across a version that changed/clarified the TOKEN and finding a hand-maintained mirror now diverges. Fuzzing or property-testing a custom Serializer against serde_json's value Serializer with arbitrary struct names that happen to collide.
Related errors
AI-assisted analysis of serde-rs/json@afdf6fc672 (2026-08-08).
Data as JSON: /api/errors/e8f13afcca202c9f.
Report an issue: GitHub.