diff --git a/crates/util/edcode-derive-tests/Cargo.toml b/crates/util/edcode-derive-tests/Cargo.toml
new file mode 100644
index 00000000..82357c97
--- /dev/null
+++ b/crates/util/edcode-derive-tests/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "rimecraft-edcode-derive-tests"
+version = "0.1.0"
+edition = "2021"
+authors = ["C191239 <zhushunzhong2025@i.pkuschool.edu.cn>"]
+description = "Tests for rimecraft-edcode-derive"
+repository = "https://github.com/rimecraft-rs/rimecraft/"
+license = "AGPL-3.0-or-later"
+categories = ["encoding"]
+
+[badges]
+maintenance = { status = "passively-maintained" }
+
+[dev-dependencies]
+rimecraft-edcode = { path = "../edcode", features = ["derive"] }
+
+[lints]
+workspace = true
diff --git a/crates/util/edcode-derive-tests/src/lib.rs b/crates/util/edcode-derive-tests/src/lib.rs
new file mode 100644
index 00000000..851525a2
--- /dev/null
+++ b/crates/util/edcode-derive-tests/src/lib.rs
@@ -0,0 +1,23 @@
+//! Tests for `rimecraft-edcode-derive` crate.
+
+#[cfg(test)]
+mod tests {
+    use rimecraft_edcode::{bytes::BytesMut, Decode, Encode};
+
+    #[test]
+    #[allow(dead_code)]
+    fn derive_enum() {
+        #[derive(Encode, Decode, PartialEq, Eq)]
+        #[repr(u8)]
+        enum Topics {
+            Pearl = 15,
+            Lakers = 24,
+            Kim = 3,
+            Someone = 36,
+        }
+
+        let mut buf = BytesMut::new();
+        assert!(Topics::Someone.encode(&mut buf).is_ok());
+        assert!(Topics::decode(buf).is_ok_and(|x| x == Topics::Someone));
+    }
+}
diff --git a/crates/util/edcode-derive/Cargo.toml b/crates/util/edcode-derive/Cargo.toml
new file mode 100644
index 00000000..7ff97052
--- /dev/null
+++ b/crates/util/edcode-derive/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "rimecraft-edcode-derive"
+version = "0.1.0"
+edition = "2021"
+authors = ["C191239 <zhushunzhong2025@i.pkuschool.edu.cn>"]
+description = "Derive macros for rimecraft-edcode"
+repository = "https://github.com/rimecraft-rs/rimecraft/"
+license = "AGPL-3.0-or-later"
+categories = ["encoding"]
+
+[badges]
+maintenance = { status = "passively-maintained" }
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+syn = "2.0"
+quote = "1.0"
+proc-macro2 = "1.0"
+
+[lints]
+workspace = true
+
+[lib]
+proc-macro = true
diff --git a/crates/util/edcode-derive/src/lib.rs b/crates/util/edcode-derive/src/lib.rs
new file mode 100644
index 00000000..bac2a9f0
--- /dev/null
+++ b/crates/util/edcode-derive/src/lib.rs
@@ -0,0 +1,209 @@
+//! Proc-macros for deriving `rimecraft_edcode` traits.
+//!
+//! __You shouldn't use this crate directly__, use `rimecraft_edcode` crate
+//! with `derive` feature flag instead.
+
+use proc_macro::TokenStream;
+use proc_macro2::TokenTree;
+use quote::quote;
+use syn::{
+    parse_macro_input, spanned::Spanned, Attribute, Data, DataEnum, DeriveInput, Error, Expr,
+    Fields, Ident, Meta,
+};
+
+macro_rules! unsupported_object {
+    ($tr:literal, $ty:literal) => {
+        concat!("deriving `", $tr, "` to `", $ty, "` is not supported")
+    };
+}
+
+macro_rules! fields_disallowed {
+    () => {
+        "variants with fields are not supported"
+    };
+}
+
+macro_rules! discriminant_required {
+    () => {
+        "variants must have explicit discriminant"
+    };
+}
+
+macro_rules! unsupported_repr {
+    () => {
+        "only `u*` and `i*` (excluding 128-bit types) repr is supported"
+    };
+}
+
+macro_rules! repr_required {
+    () => {
+        "must specify repr"
+    };
+}
+
+/// Common parsing code for deriving to `enum`.
+fn parse_derive_enum(
+    ident: Ident,
+    attrs: Vec<Attribute>,
+    data: DataEnum,
+) -> Result<(Ident, Ident, Vec<Ident>, Vec<Expr>), TokenStream> {
+    let mut enum_idents: Vec<Ident> = vec![];
+    let mut enum_vals: Vec<Expr> = vec![];
+    for var in data.variants.into_iter() {
+        if matches!(var.fields, Fields::Unit) {
+            enum_idents.push(var.ident.clone());
+        } else {
+            return Err(Error::new(var.fields.span(), fields_disallowed!())
+                .into_compile_error()
+                .into());
+        }
+        let has_disc_err = !var.discriminant.clone().is_some_and(|(_, e)| {
+            enum_vals.push(e);
+            true
+        });
+        if has_disc_err {
+            return Err(Error::new(var.span(), discriminant_required!())
+                .into_compile_error()
+                .into());
+        }
+    }
+    let mut repr_type: Option<Ident> = None;
+    for attr in attrs {
+        if let Meta::List(meta) = attr.meta {
+            let is_repr = meta
+                .path
+                .require_ident()
+                .is_ok_and(|id| id == &Ident::new("repr", id.span()));
+            if is_repr {
+                let mut iter = meta.tokens.into_iter().peekable();
+                let span = iter.peek().span();
+                macro_rules! ident_helper {
+                    ($span:expr => $( $ty:ident ),*) => {
+                        vec![
+                            $( Ident::new(stringify!($ty), $span) ),*
+                        ]
+                    };
+                }
+                let supported = iter.next().is_some_and(|x| {
+                    if let TokenTree::Ident(id) = x {
+                        if ident_helper!(span => u8, u16, u32, u64, i8, i16, i32, i64).contains(&id)
+                        {
+                            repr_type = Some(id);
+                            true
+                        } else {
+                            false
+                        }
+                    } else {
+                        false
+                    }
+                });
+                if !supported {
+                    return Err(Error::new(span, unsupported_repr!())
+                        .into_compile_error()
+                        .into());
+                }
+            }
+        }
+    }
+    let repr_type = repr_type.ok_or_else(|| {
+        std::convert::Into::<TokenStream>::into(
+            Error::new(ident.span(), repr_required!()).into_compile_error(),
+        )
+    })?;
+    Ok((ident, repr_type, enum_idents, enum_vals))
+}
+
+/// Derive `rimecraft_edcode::Encode` to objects.
+///
+/// # Enum
+///
+/// ## Requirements:
+/// - All variants must be field-less.
+/// - Enum must explicitly specify its representation through `#[repr()]`, and
+///   only `u*` and `i*` (excluding 128-bit types) repr are allowed.
+/// - All variants must have explicit discriminant.
+#[proc_macro_derive(Encode)]
+pub fn derive_encode(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    match input.data {
+        Data::Enum(data) => {
+            let (ident, repr_type, enum_idents, enum_vals) =
+                match parse_derive_enum(input.ident, input.attrs, data) {
+                    Ok(x) => x,
+                    Err(err) => return err,
+                };
+            let expanded = quote! {
+                impl ::rimecraft_edcode::Encode for #ident {
+                    fn encode<B>(&self, mut buf: B) -> Result<(), std::io::Error>
+                    where
+                        B: ::rimecraft_edcode::bytes::BufMut,
+                    {
+                        let x:#repr_type = match self {
+                            #( Self::#enum_idents => #enum_vals, )*
+                        };
+                        ::rimecraft_edcode::Encode::encode(&x, &mut buf)?;
+                        Ok(())
+                    }
+                }
+            };
+            expanded.into()
+        }
+        Data::Struct(data) => Error::new(
+            data.struct_token.span,
+            unsupported_object!("Encode", "struct"),
+        )
+        .to_compile_error()
+        .into(),
+        Data::Union(data) => Error::new(
+            data.union_token.span,
+            unsupported_object!("Encode", "union"),
+        )
+        .to_compile_error()
+        .into(),
+    }
+}
+
+/// Derive `rimecraft_edcode::Decode` to objects.
+#[proc_macro_derive(Decode)]
+pub fn derive_decode(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
+    match input.data {
+        Data::Enum(data) => {
+            let (ident, repr_type, enum_idents, enum_vals) =
+                match parse_derive_enum(input.ident, input.attrs, data) {
+                    Ok(x) => x,
+                    Err(err) => {
+                        return err;
+                    }
+                };
+            let expanded = quote! {
+                impl ::rimecraft_edcode::Decode for #ident {
+                    fn decode<B>(mut buf: B) -> Result<Self, std::io::Error>
+                    where
+                        B: ::rimecraft_edcode::bytes::Buf,
+                    {
+                        let x:#repr_type = ::rimecraft_edcode::Decode::decode(&mut buf)?;
+                        let var = match x {
+                            #( #enum_vals => Self::#enum_idents, )*
+                            unknown => return Err(std::io::Error::other(format!("unknown variant {}", unknown))),
+                        };
+                        Ok(var)
+                    }
+                }
+            };
+            expanded.into()
+        }
+        Data::Struct(data) => Error::new(
+            data.struct_token.span,
+            unsupported_object!("Decode", "struct"),
+        )
+        .to_compile_error()
+        .into(),
+        Data::Union(data) => Error::new(
+            data.union_token.span,
+            unsupported_object!("Decode", "union"),
+        )
+        .into_compile_error()
+        .into(),
+    }
+}
diff --git a/crates/util/edcode/Cargo.toml b/crates/util/edcode/Cargo.toml
index 2c0cdfb1..e9764ba2 100644
--- a/crates/util/edcode/Cargo.toml
+++ b/crates/util/edcode/Cargo.toml
@@ -14,6 +14,7 @@ maintenance = { status = "passively-maintained" }
 [dependencies]
 bytes = "1.5"
 serde = { version = "1.0", optional = true }
+rimecraft-edcode-derive = { path = "../edcode-derive", optional = true }
 # custom formats
 fastnbt = { version = "2.4", optional = true }
 serde_json = { version = "1.0", optional = true }
@@ -24,6 +25,7 @@ glam = { version = "0.25", optional = true }
 [features]
 # default = ["serde", "nbt", "json", "uuid", "glam"]
 serde = ["dep:serde"]
+derive = ["dep:rimecraft-edcode-derive"]
 # custom formats
 fastnbt = ["serde", "dep:fastnbt"]
 json = ["serde", "dep:serde_json"]
diff --git a/crates/util/edcode/src/lib.rs b/crates/util/edcode/src/lib.rs
index 0eedda4f..57838c3e 100644
--- a/crates/util/edcode/src/lib.rs
+++ b/crates/util/edcode/src/lib.rs
@@ -9,6 +9,9 @@ use std::io;
 
 pub use bytes;
 
+#[cfg(feature = "derive")]
+pub use rimecraft_edcode_derive::{Decode, Encode};
+
 /// Describes types that can be encoded into a packet buffer.
 pub trait Encode {
     /// Encode into a buffer.