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
- 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.
- 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).
- 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.
- 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
- Only apply #[derive(ContentHash)] to structs and enums; never copy the derive list onto a union item.
- If a type exists only for C interop or bit reinterpretation (typical union use), keep it out of the content-addressed model entirely — jj never needs to hash raw FFI payloads.
- When a value has several layouts, model it as an enum: the derive then hashes the variant ordinal plus fields, which is a stable, documented scheme.
- Any hand-written ContentHash impl must be byte-stable forever once persisted, since jj stores rely on it for object identity — write it once, deliberately, with a test pinning the hash output.
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.