Rust Sealed Traits
A sealed trait is a public trait that only its own crate can implement. The seal is a private supertrait that outside code cannot name.
The shape: a public trait with a private supertrait
Measure is public. Its supertrait lives in a private module — and that is the whole trick.
pub trait Measure: private::Sealed {
fn label(&self) -> String;
}The private module
Sealed is a pub trait, but its module is private, so outside code has no path to write. No path means no impl — ever.
mod private {
pub trait Sealed {}
}At home, your types get both impls
Only types in this crate receive the Sealed impl, so only they can satisfy Measure.
impl private::Sealed for Grams {}
impl Measure for Grams {
fn label(&self) -> String {
format!("{} g", self.amount)
}
}Outside, the wall is error E0277
A type that never received the Sealed impl is refused. The error reads “the trait bound `Pounds: Sealed` is not satisfied” (checked with the compiler).
impl Measure for Pounds {
// refused — Pounds does not implement private::Sealed
}Example
mod private {
// A public trait on an unreachable path:
// only this file can name it, so only this file can implement it.
pub trait Sealed {}
}
// Measure is public and usable everywhere —
// but only types listed here can ever implement it.
pub trait Measure: private::Sealed {
fn label(&self) -> String;
}
pub struct Grams {
pub amount: u32,
}
pub struct Kilograms {
pub amount: u32,
}
impl private::Sealed for Grams {}
impl private::Sealed for Kilograms {}
impl Measure for Grams {
fn label(&self) -> String {
format!("{} g", self.amount)
}
}
impl Measure for Kilograms {
fn label(&self) -> String {
format!("{} kg", self.amount)
}
}
// Anyone may USE the trait as a bound — sealing blocks implementing, not using.
fn describe<T: Measure>(item: &T) -> String {
format!("measure: {}", item.label())
}
fn main() {
let flour = Grams { amount: 500 };
let rice = Kilograms { amount: 2 };
println!("{}", describe(&flour));
println!("{}", describe(&rice));
}Key points
- Sealing blocks implementing, not using: anyone may call the methods and write T: Measure bounds
- Why seal: adding a method to a public trait is normally a breaking change.
- With a sealed trait it is safe.
- Every implementor lives in your crate
- Real users of the pattern: the Index trait in serde_json and the ByteOrder trait in byteorder are both sealed
- Say so in the docs.
- rustdoc should state plainly that the trait is sealed.
- Users then waste no time trying to implement it
- A sealed trait can be used anywhere but implemented only in its own crate.
In this example, one small file plays both library and user. The private module holds Sealed. The module is not pub, so no outside code can name the trait. No outside type can ever earn the Sealed impl that Measure demands. Grams and Kilograms got both impls here at home, so they work. And describe shows the other half of the pattern. Sealing never blocks use. A generic function over T: Measure is perfectly happy.
Note
Sealing is one-way.
You may unseal a trait in a later release.
That breaks nothing.
Sealing an open trait afterwards breaks every outside implementor.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
What does sealing a trait prevent?