denoland/deno · error
cannot use non-object value with an internally tag enum
Error message
cannot use non-object value with an internally tag enum
What it means
deno_core's #[op2] codegen converts Rust enums marked #[serde(tag = "...")] (internally tagged) into v8 objects by inserting the tag key into the serialized variant body. The generated code requires that body to convert to a v8::Object; when a variant serializes to a non-object value (string, number, array), it panics with this message.
Source
Thrown at libs/ops/conversion/to_v8/enum.rs:120
Ok(::deno_core::v8::Object::with_prototype_and_properties(
__scope,
__null,
__keys,
__converters,
).into())
}
}
EnumMode::InternallyTagged { tag } => {
quote! {
if Ok(__obj_body) = __body.try_cast::<::deno_core::v8::Object>() {
let __tag_key = #tag;
let __tag_value = #tag_value;
__obj_body.set(__scope, __tag_key, __tag_value);
Ok(__obj_body.into())
} else {
panic!("cannot use non-object value with an internally tag enum");
}
}
}
EnumMode::AdjacentlyTagged { tag, content } => {
quote! {
let __null = ::deno_core::v8::null(__scope).into();
let __keys = &[#tag, #content];
let __converters = &[#tag_value, __body];
Ok(::deno_core::v8::Object::with_prototype_and_properties(
__scope,
__null,
__keys,
__converters,
).into())
}
}
EnumMode::Untagged => {View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Switch to adjacently tagged encoding: #[serde(tag = "...", content = "...")], which the codegen handles by wrapping the body in an object
- Restructure the enum so every data-carrying variant is a struct variant (serializes as a map)
- Pass the value as a plain struct or serde_json::Value instead of a tagged enum across the op boundary
- If the enum is unit-only, verify every variant path produces the tag object before calling the op
Example fix
// before
#[derive(Serialize)]
#[serde(tag = "kind")]
enum Event {
Ping(u64), // body is a number -> panic in generated code
}
// after
#[derive(Serialize)]
#[serde(tag = "kind", content = "data")]
enum Event {
Ping(u64), // { "kind": "Ping", "data": 1 }
} Defensive patterns
Strategy: validation
Validate before calling
// Before returning a tagged enum from an op, confirm it serializes object-shaped
let v = serde_json::to_value(&event).map_err(|e| e.to_string())?;
if !v.is_object() {
// restructure the enum (adjacently tag it) before crossing the op boundary
return Err("internally tagged enum variant is not object-shaped");
} Type guard
fn serializes_as_object<T: serde::Serialize>(v: &T) -> bool {
serde_json::to_value(v).map(|j| j.is_object()).unwrap_or(false)
} Prevention
- Prefer adjacently tagged enums ([serde(tag = ..., content = ...)]) in op signatures
- Add a unit test per op signature that round-trips every enum variant through serialization
- Keep enums crossing op boundaries limited to struct variants and unit variants
When it happens
Trigger: Defining an op whose parameter or return type is an internally tagged enum where a variant's serialized payload is not object-shaped - e.g. a newtype variant wrapping a primitive, or a variant whose serde serialization yields a scalar instead of a map.
Common situations: Sharing serde enum types between HTTP handlers and deno ops; adding #[serde(tag)] to an existing enum already crossing an op boundary; refactoring struct variants into tuple variants.
Related errors
- Cannot use serde on unit variant
- FromV8 enum derive currently supports only unit and single-e
- Cannot combine `untagged` with `tag` or `content`
- `content` requires `tag` to be specified
- Only ASCII keys are supported
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/05c4a71968190518.
Report an issue: GitHub.