pydantic/monty · error
ObjectList is decode-only
Error message
ObjectList is decode-only
What it means
`ObjectList` in `crates/monty-proto/src/wire.rs` is a hand-written `prost::Message` that exists only for decoding recursive nested lists (values are validated while decoding and mapped straight into `MontyObject`s). Its `encode_raw` is therefore never called: any object list travelling parent→child is encoded from the borrowed `MontyObject` itself, not from this type. The `unreachable!()` fires only if a decode-only wire type is accidentally used as an encode source — an internal `monty-proto` bug.
Source
Thrown at crates/monty-proto/src/wire.rs:884
impl Message for ObjectList {
fn merge_field(
&mut self,
tag: u32,
wire_type: WireType,
buf: &mut impl Buf,
ctx: DecodeContext,
) -> Result<(), DecodeError> {
// `ObjectList.items` is field 1; any other tag is unknown → skip.
if tag == 1 {
merge_object_item(wire_type, buf, ctx, &mut self.0)
} else {
skip_field(wire_type, tag, buf, ctx)
}
}
fn encode_raw(&self, _buf: &mut impl BufMut) {
unreachable!("ObjectList is decode-only")
}
fn encoded_len(&self) -> usize {
unreachable!("ObjectList is decode-only")
}
fn clear(&mut self) {
self.0.clear();
}
}
/// Decode-only `prost::Message` that materializes a `repeated Pair` field
/// directly into `(key, value)` tuples — the dict analogue of [`ObjectList`],
/// avoiding the `Vec<pb::Pair>` wrapper. Decode-only; encode is unreachable.
#[derive(Default)]
struct PairList(Vec<(MontyObject, MontyObject)>);
impl Message for PairList {View on GitHub (pinned to adc986b362)
Solutions
- Never encode from `ObjectList`; re-encode from the original `MontyObject`/`WireObject` that the list was decoded from
- If a type must support both directions, implement real `encode_raw`/`encoded_len` instead of the decode-only stub
- Convert any test or code path holding an `ObjectList` back into `MontyObject` before encoding
- Run `make check-proto` and `cargo test -p monty-proto` (differential tests) after protocol changes
Example fix
// before let bytes = ObjectList::from(items).encode_to_vec(); // panics: decode-only // after let wire_obj = WireObject::from(&monty_object); let bytes = wire_obj.encode_to_vec();
Defensive patterns
Strategy: type-guard
Validate before calling
fn ensure_encodable<T: prost::Message + Encodable>(_: &T) {} // only pass WireObject/MontyObject-backed types to encoders Type guard
fn is_decode_only_aggregate(msg: &dyn std::any::Any) -> bool {
msg.is::<ObjectList>() || msg.is::<PairList>() || msg.is::<TypeBody>() || msg.is::<NamedTupleBody>()
} Try / catch
// Prefer static avoidance: never call encode on decode-only types; if generic code must encode, // convert to the canonical encodable type first let wire = WireObject::from(&monty_object); wire.encode_to_vec()
Prevention
- Encode only from `WireObject` (or borrowed `MontyObject`) in parent→child messages
- Keep decode-only aggregates out of any field of serialized messages
- Add a comment on the type (`/// decode-only — encode from WireObject`) and enforce via type boundaries
- Run `make check-proto` and the differential tests when touching the wire layer
When it happens
Trigger: Calling `encode_raw` (directly or via `Message::encode`/`encode_length_delimited`) on an `ObjectList` value, e.g. by routing decoded nested values back through the wire encoder in error paths or tests.
Common situations: Seen while developing the wire protocol: constructing differential-test messages from decoded values, or changing the encoder to reuse the aggregate types for both directions.
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
- PairList is decode-only
- TypeBody is decode-only
- NamedTupleBody is decode-only
- varargs/varkwargs own no param slot
- checked above
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/b481b0ad39930861.
Report an issue: GitHub.