jj-vcs/jj · error

ContentHash cannot be derived for unions.

Error message

ContentHash cannot be derived for unions.

What it means

This is a compile-time panic raised inside the `ContentHash` derive macro from the Jujutsu (jj) version-control library. The macro walks the derived type with `syn` and generates hash code for structs (named, tuple, unit) and enums, but when it sees `Data::Union` it hits an explicit `unimplemented!()` at lib/proc-macros/src/content_hash.rs:101, so `cargo check`/`build` fails with a 'proc-macro panicked' error. `ContentHash` produces the stable content-addressed hashes jj uses for commits, operations, and other backend objects, and the project deliberately chose not to define a hashing scheme for unions.

Source

Thrown at lib/proc-macros/src/content_hash.rs:101

                        }
                    }
                    Fields::Unit => {
                        let ix = index_to_ordinal(i);
                        quote_spanned! {v.span() =>
                            Self::#variant_id => {
                                ::jj_lib::content_hash::ContentHash::hash(&#ix, state);
                            }
                        }
                    }
                }
            });
            quote! {
                match self {
                    #(#match_hash_statements)*
                }
            }
        }
        Data::Union(_) => unimplemented!("ContentHash cannot be derived for unions."),
    }
}

// The documentation for `ContentHash` specifies that the hash impl for each
// enum variant should hash the ordinal number of the enum variant as a little
// endian u32 before hashing the variant's fields, if any.
fn index_to_ordinal(ix: usize) -> u32 {
    u32::try_from(ix).expect("The number of enum variants overflows a u32.")
}

fn enum_bindings_with_type<'a>(fields: impl IntoIterator<Item = &'a Field>) -> Vec<(Type, Ident)> {
    fields
        .into_iter()
        .enumerate()
        .map(|(i, f)| {
            // If the field is named, use the name, otherwise generate a placeholder name.
            (
                f.ty.clone(),

View on GitHub (pinned to 7fa941edb4)

Solutions

  1. Convert the union into a struct or an enum; the derive fully supports `Data::Struct` and `Data::Enum`, and an enum with one variant per union arm also hashes an ordinal (little-endian u32) per variant, matching jj's hash scheme.
  2. If the union must remain (e.g. for POD/FFI layout), remove `#[derive(ContentHash)]` from it and hand-write the `impl jj_lib::content_hash::ContentHash for MyUnion` — typically hashing a discriminant byte followed by a stable byte serialization of the fields (e.g. via `bytemuck` of the POD payload).
  3. Keep the union untouched and wrap it in a newtype struct: derive `ContentHash` on the wrapper only if all its other fields implement it, otherwise implement the trait manually for the wrapper while delegating to the inner union's manual byte hashing.
  4. If you need this supported generally, implement a `Data::Union` arm in lib/proc-macros/src/content_hash.rs (e.g. hashing the raw field bytes) and upstream it to the jj repo — currently there is deliberately no scheme, so any manual impl must be stable forever once written to a store.

Example fix

// before
#[derive(ContentHash, Clone, PartialEq, Eq)]
union Header {
    raw: u32,
    parts: Parts, // compile fails: proc-macro panicked at 'ContentHash cannot be derived for unions.'
}

// after: replace the union with a struct (or enum) so the derive works
#[derive(ContentHash, Clone, PartialEq, Eq)]
struct Header {
    raw: u32,
}

// or: keep the union, write the impl by hand
union Header {
    raw: u32,
    parts: Parts,
}
impl jj_lib::content_hash::ContentHash for Header {
    fn hash(&self, state: &mut blake2b_simd::Hasher) {
        // hash a stable representation of the active variant
        jj_lib::content_hash::ContentHash::hash(&unsafe { self.raw }, state);
    }
}
Defensive patterns

Strategy: validation

Validate before calling

# CI pre-check: fail if ContentHash is ever derived on a union before cargo build does
rg -U --multiline '#\[derive\([^)]*ContentHash[^)]*\)\]\s*(?:pub\s+)?union' crates/ && { echo 'ContentHash cannot be derived on unions'; exit 1; } || exit 0

Type guard

// Compile-time guard placed next to the type: derive only when the shape is supported.
// (Rust has no reflection over derive targets; guard by construction instead —)
// restrict ContentHash to struct/enum shapes via a macro that rejects unions:
macro_rules! content_hashable {
    (struct $($rest:tt)*) => { #[derive(::jj_lib::content_hash::ContentHash)] $($rest)* };
    (enum $($rest:tt)*)   => { #[derive(::jj_lib::content_hash::ContentHash)] $($rest)* };
    (union $($rest:tt)*)  => { compile_error!("ContentHash cannot be derived for unions; use a struct/enum or a manual impl"); };
}

Try / catch

null — this panic happens inside the proc macro at compile time and aborts `cargo check`/`build`; it cannot be caught with std::panic::catch_unwind or any runtime handler.

Prevention

When it happens

Trigger: Writing `#[derive(ContentHash)]` (the proc macro exported from lib/proc-macros/src/lib.rs:14, used via `jj_lib::content_hash`) on any Rust `union` item. The derive expansion calls `generate_hash_impl(&Data)`, the `Data::Union(_)` arm matches, and the macro panics immediately during compilation — no runtime code is ever executed.

Common situations: Bringing existing FFI or bit-reinterpretation code (unions are common for C interop and zero-cost transmutes) into a jj-based project where the type must become content-hashable; copying a derive list like `#[derive(ContentHash, Clone, PartialEq, ...)]` from a struct to a union assuming derive macros treat unions like structs (serde has union support, so developers expect it here); newer jj versions where more internal types derive `ContentHash`, tempting users to add it to a union-typed edge case.


AI-assisted analysis of jj-vcs/jj@7fa941edb4 (2026-08-16). Data as JSON: /api/errors/009f80cd8e655380. Report an issue: GitHub.