diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -527,6 +527,8 @@ version = "0.1.0"
 dependencies = [
  "moxcms",
  "rayon",
+ "serde",
+ "serde_json",
 ]
 
 [[package]]
diff --git a/crates/cms/Cargo.toml b/crates/cms/Cargo.toml
--- a/crates/cms/Cargo.toml
+++ b/crates/cms/Cargo.toml
@@ -12,8 +12,16 @@ exclude = ["tests/data", "tests/fogra39.rs"]
 [lints]
 workspace = true
 
+[features]
+default = []
+serde = ["dep:serde"]
+
+[dependencies]
+serde = { version = "1", features = ["derive"], optional = true }
+
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 rayon = { workspace = true }
 
 [dev-dependencies]
 moxcms = "0.8"
+serde_json = "1"
diff --git a/crates/cms/src/lib.rs b/crates/cms/src/lib.rs
--- a/crates/cms/src/lib.rs
+++ b/crates/cms/src/lib.rs
@@ -39,6 +39,8 @@ pub use transform::{SampleKind, Transform, TransformOptions, black_point, cached
 
 /// ICC rendering intent.
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
 pub enum Intent {
     Perceptual = 0,
     #[default]
diff --git a/crates/cms/src/transform.rs b/crates/cms/src/transform.rs
--- a/crates/cms/src/transform.rs
+++ b/crates/cms/src/transform.rs
@@ -20,7 +20,15 @@ pub enum SampleKind {
 }
 
 /// Options for building a [`Transform`].
+///
+/// When compiled with the `serde` feature, this struct implements [`Serialize`](serde::Serialize)
+/// and [`Deserialize`](serde::Deserialize) with kebab-case field naming and defaults for omitted fields.
+/// This format is intended for transient configuration interchange (such as IPC payloads, command-line
+/// options, and temporary rendering preferences). Deserialization is backward-compatible with omitted
+/// fields via `#[serde(default)]`.
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
 pub struct TransformOptions {
     pub intent: Intent,
     /// Adobe-style black point compensation (ignored for absolute colorimetric).
diff --git a/crates/cms/tests/serde.rs b/crates/cms/tests/serde.rs
new file mode 100644
--- /dev/null
+++ b/crates/cms/tests/serde.rs
@@ -0,0 +1,42 @@
+#![cfg(feature = "serde")]
+
+use craft_cms::{Intent, TransformOptions};
+
+#[test]
+fn test_serde_intent_kebab_case() {
+    let intents = [
+        (Intent::Perceptual, "\"perceptual\""),
+        (Intent::RelativeColorimetric, "\"relative-colorimetric\""),
+        (Intent::Saturation, "\"saturation\""),
+        (Intent::AbsoluteColorimetric, "\"absolute-colorimetric\""),
+    ];
+
+    for (intent, json) in intents {
+        let serialized = serde_json::to_string(&intent).expect("serialize intent");
+        assert_eq!(serialized, json);
+        let deserialized: Intent = serde_json::from_str(json).expect("deserialize intent");
+        assert_eq!(deserialized, intent);
+    }
+}
+
+#[test]
+fn test_serde_transform_options_defaults() {
+    let empty_json = "{}";
+    let opts: TransformOptions = serde_json::from_str(empty_json).expect("deserialize empty options");
+    assert_eq!(opts, TransformOptions::default());
+
+    let partial_json = r#"{"intent": "perceptual"}"#;
+    let opts_partial: TransformOptions = serde_json::from_str(partial_json).expect("deserialize partial");
+    assert_eq!(opts_partial.intent, Intent::Perceptual);
+    assert!(opts_partial.bpc);
+    assert!(opts_partial.precise_float);
+
+    let full_json = r#"{"intent":"saturation","bpc":false,"precise-float":false}"#;
+    let opts_full: TransformOptions = serde_json::from_str(full_json).expect("deserialize full");
+    assert_eq!(opts_full.intent, Intent::Saturation);
+    assert!(!opts_full.bpc);
+    assert!(!opts_full.precise_float);
+
+    let reserialized = serde_json::to_string(&opts_full).expect("serialize options");
+    assert_eq!(reserialized, full_json);
+}
