When the compiler translates for you
You write a function that takes a string slice. You have a String. You pass the String. The compiler doesn't complain. You wrap that String in a custom struct to add metadata. You pass the wrapper. The compiler still stays quiet. It feels like the compiler is guessing your intent, but it's actually following a strict set of rules called coercion.
Coercion is automatic type conversion. The compiler inserts invisible adapters so your code compiles without boilerplate. You don't write conversion methods. You don't call .into(). You just pass the value, and Rust bridges the gap.
The universal adapter
Think of coercion like a universal power adapter in your pocket. You have a device with a specific plug. The wall socket has a different shape. You don't rewire the device. You don't change the socket. You just pull out the adapter, click it in, and the power flows. The adapter is invisible to the operation of the device.
Rust's compiler carries these adapters for you. It knows which plugs fit which sockets and inserts the conversion automatically at the call site. There are two main families of coercion you'll encounter.
Deref coercion handles smart pointers. It lets you pass a Box<T>, Rc<T>, or a custom wrapper where a reference to T is expected. Unsized coercion handles types that don't have a fixed size. It lets you pass a String where a &str is needed, or an array where a slice is expected.
Both mechanisms rely on the compiler knowing the target type. If the compiler can't see what type is expected, it won't coerce. This distinction separates coercion from casting. Casting forces a conversion. Coercion adapts to the context.
Minimal example
Here's a concrete case. You have a wrapper struct that holds a String. You want to pass it to a function that expects &str.
use std::ops::Deref;
/// Wraps a String to demonstrate automatic unwrapping.
struct Wrapper(String);
impl Deref for Wrapper {
type Target = String;
fn deref(&self) -> &Self::Target {
// Return a reference to the inner String.
&self.0
}
}
/// Prints any string slice.
fn print_it(s: &str) {
println!("{s}");
}
fn main() {
let w = Wrapper("Hello".to_string());
// Deref coercion: &Wrapper -> &String -> &str
print_it(&w);
let s = String::from("World");
// Unsized coercion: String -> &str
print_it(&s);
}
The first call passes &w, which has type &Wrapper. The function expects &str. The compiler sees Wrapper implements Deref with Target = String. It coerces &Wrapper to &String. Then it sees String implements Deref with Target = str. It coerces &String to &str. The chain completes.
The second call passes &s, which is &String. String is a sized type. str is unsized. The compiler coerces &String to &str by taking a slice of the entire string buffer. This is unsized coercion.
Trust the chain. If the types line up, Rust will bridge the gap without you writing a single conversion method.
How the compiler decides
Coercion only happens at specific locations called coercion sites. The compiler must know the target type to perform the conversion. Common coercion sites include function arguments, return values, let bindings with explicit type annotations, and match arms.
When you write print_it(&w), the compiler looks at the signature of print_it. It sees the parameter is &str. It checks &w. It's &Wrapper. It checks if Wrapper can coerce to str. It finds the Deref chain. It inserts the coercion.
If you remove the function call and just write let x = &w;, coercion doesn't happen. The compiler has no target type. x becomes &Wrapper. If you later try to pass x to print_it, the compiler checks &Wrapper against &str. It might still coerce, but the binding itself didn't coerce.
This behavior catches everyone off guard at first. Coercion is reactive. It responds to the type the compiler expects. It doesn't proactively change types in variable declarations.
Convention aside: The community treats Deref as a wrapper contract. If your type holds a value and exposes it, Deref makes sense. If you implement Deref just to make a function call compile, you're doing Deref abuse. It confuses readers and slows compilation. Use AsRef for explicit conversions instead. AsRef requires the caller to write .as_ref(), which signals intent. Deref hides intent.
Realistic scenario: Event payloads
Imagine you're building an event system. Events come from a message queue as boxed strings. You wrap them in a struct to add timestamps. Your handler functions expect string slices for efficiency.
use std::ops::Deref;
/// Represents a parsed event payload from the queue.
struct EventPayload {
data: Box<String>,
timestamp: u64,
}
impl Deref for EventPayload {
type Target = String;
fn deref(&self) -> &Self::Target {
// Expose the inner String for coercion.
&self.data
}
}
/// Processes an event message.
fn process_event(msg: &str) {
println!("Processing: {msg}");
}
fn main() {
let payload = EventPayload {
data: Box::new("User login".to_string()),
timestamp: 1234567890,
};
// Coercion chain: &EventPayload -> &String -> &str
// Box<String> also implements Deref, but the chain stops at String
// because String derefs to str.
process_event(&payload);
}
The call process_event(&payload) works because of the chain. &EventPayload coerces to &String via your Deref impl. &String coerces to &str via String's Deref impl. The Box inside EventPayload doesn't participate in the coercion chain directly. Your Deref returns &String, not &Box<String>. If you returned &self.data directly, the type would be &Box<String>, and the chain would be &EventPayload -> &Box<String> -> &String -> &str. Box implements Deref, so the chain extends.
Deref coercion can chain arbitrarily deep. The compiler follows Deref impls until it finds a match or exhausts the chain. This flexibility is powerful but requires discipline. Deep chains make code harder to trace. Keep wrappers shallow.
Don't implement Deref just to enable coercion. Implement it when your type is logically a wrapper. If the relationship is "has-a" rather than "is-a-wrapper", use explicit methods or AsRef.
Pitfalls and compiler errors
Coercion has rules. Breaking them produces errors that look like type mismatches but are really coercion failures.
The most common error is E0308 (mismatched types) in a let binding. You write let s = &my_string; and expect &str. You get &String. The compiler rejects this with E0308 if you try to use s where &str is expected. The fix is a type annotation. let s: &str = &my_string; gives the compiler a target. Coercion kicks in.
Mutable coercion is stricter. Deref coercion only works for immutable references. If you need &mut, the type must implement DerefMut. If you pass &mut wrapper to a function expecting &mut T, and Wrapper only implements Deref, the compiler rejects it. You'll see an error about mutable borrows. Implement DerefMut to enable mutable coercion.
Another trap is coercion in generic contexts. Generics delay type resolution. Coercion can fail if the compiler can't determine the concrete type early enough. You might see E0277 (trait bound not satisfied) when a coercion is expected but the generic parameter hasn't been resolved. This often happens with trait objects. Box<dyn Trait> involves unsized coercion. The compiler must coerce a concrete type to the trait object. If the trait isn't object-safe or the bounds are missing, coercion fails.
Coercion doesn't happen in struct initialization. If you have a struct field of type &str, you can't assign a String directly. You must coerce at the call site or use .as_str(). The compiler won't coerce inside the struct literal.
Coercion is a convenience, not a guarantee. If the compiler can't see the target, it won't convert. Annotate the type when the context is missing.
When to use coercion
Rust provides multiple ways to convert types. Coercion is one tool. Pick the right tool based on the relationship between types and the context.
Use Deref coercion when you have a smart pointer like Box, Rc, or a custom wrapper and the function expects a reference to the inner type. The wrapper should logically expose the inner value.
Use Unsized coercion when you have a sized type like String or [T; N] and the function expects a slice like &str or &[T]. This is the standard way to pass owned collections to functions that don't need ownership.
Use explicit .as_ref() when you need to convert types in a context where coercion doesn't happen, such as inside a method chain or a generic bound. AsRef is explicit. It signals intent to readers and works where coercion is silent.
Reach for type annotations when the compiler reports a mismatched type error in a variable declaration. Adding : &str or : &[T] gives the compiler the target it needs to coerce.
Avoid implementing Deref for types that are not logical wrappers. Implementing Deref creates implicit behavior that can surprise callers. If the type isn't a wrapper, use methods or AsRef.
Pick the tool that matches the relationship. Coercion hides conversion. Explicit conversion reveals it. Use hiding for wrappers. Use revealing for transformations.