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

  1. Switch to adjacently tagged encoding: #[serde(tag = "...", content = "...")], which the codegen handles by wrapping the body in an object
  2. Restructure the enum so every data-carrying variant is a struct variant (serializes as a map)
  3. Pass the value as a plain struct or serde_json::Value instead of a tagged enum across the op boundary
  4. 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

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


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/05c4a71968190518. Report an issue: GitHub.