Loading page…
Rust walkthroughs
Loading page…
serde, how does #[serde(from = \"...\", into = \"...\")] enable custom type conversions?The #[serde(from = "T", into = "T")] attribute tells serde to perform an intermediate conversion during serialization and deserialization, using the target type's From/Into implementations to bridge between your type and a serializable representation. During deserialization, serde first deserializes into the intermediate type T, then calls YourType::from(T). During serialization, serde calls T::from(your_value) and serializes the result. This enables conversions that Serialize/Deserialize implementations alone cannot express, particularly when your type cannot directly implement those traits due to orphan rules, when you need different representations for serialize vs. deserialize, or when borrowing makes direct implementation impossible.
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone)]
struct UserId(String);
// UserId serializes as a plain string, but has type safety in code
#[derive(Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
struct UserIdWrapper(String);
impl From<String> for UserIdWrapper {
fn from(s: String) -> Self {
UserIdWrapper(s)
}
}
impl From<UserIdWrapper> for String {
fn from(id: UserIdWrapper) -> Self {
id.0
}
}
fn main() {
let id = UserIdWrapper("user-123".to_string());
let json = serde_json::to_string(&id).unwrap();
println!("Serialized: {}", json); // "user-123"
let deserialized: UserIdWrapper = serde_json::from_str(&json).unwrap();
println!("Deserialized: {}", deserialized.0);
}The from and into attributes specify the intermediate type for the conversion pipeline.
use serde::{Deserialize, Serialize};
#[derive(Debug)]
struct Percentage(u8);
#[derive(Deserialize)]
#[serde(from = "u8")] // Deserialize as u8 first
struct PercentageDeserialize(Percentage);
impl From<u8> for PercentageDeserialize {
fn from(value: u8) -> Self {
// Custom conversion logic
let validated = if value > 100 { 100 } else { value };
PercentageDeserialize(Percentage(validated))
}
}
fn main() {
// Input can be > 100, conversion clamps it
let json = "150";
let result: PercentageDeserialize = serde_json::from_str(json).unwrap();
println!("Clamped to: {}", result.0 .0); // 100
let json = "50";
let result: PercentageDeserialize = serde_json::from_str(json).unwrap();
println!("Normal value: {}", result.0 .0); // 50
}During deserialization, serde first deserializes into u8, then calls From<u8>.
use serde::{Deserialize, Serialize};
#[derive(Debug)]
struct UpperCase(String);
#[derive(Serialize)]
#[serde(into = "String")] // Convert to String before serializing
struct UpperCaseSerialize(UpperCase);
impl From<UpperCaseSerialize> for String {
fn from(value: UpperCaseSerialize) -> Self {
// Always serialize as uppercase
value.0 .0.to_uppercase()
}
}
fn main() {
let text = UpperCaseSerialize(UpperCase("hello world".to_string()));
let json = serde_json::to_string(&text).unwrap();
println!("Serialized: {}", json); // "HELLO WORLD"
}During serialization, serde calls From<Self> to get the intermediate type, then serializes that.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
struct NonEmptyString(String);
impl NonEmptyString {
fn new(s: String) -> Option<Self> {
if s.is_empty() {
None
} else {
Some(NonEmptyString(s))
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
struct NonEmpty(NonEmptyString);
impl From<String> for NonEmpty {
fn from(s: String) -> Self {
// Deserialization: use fallback for empty strings
match NonEmptyString::new(s) {
Some(valid) => NonEmpty(valid),
None => NonEmpty(NonEmptyString("[empty]".to_string())),
}
}
}
impl From<NonEmpty> for String {
fn from(value: NonEmpty) -> Self {
// Serialization: just use the inner string
value.0 .0
}
}
fn main() {
let original = NonEmpty(NonEmptyString("hello".to_string()));
let json = serde_json::to_string(&original).unwrap();
println!("Serialized: {}", json);
let empty: NonEmpty = serde_json::from_str("\"\"").unwrap();
println!("Empty becomes: {}", empty.0 .0); // "[empty]"
}When both from and into are specified, both directions use the conversion.
use serde::{Deserialize, Serialize};
#[derive(Debug)]
struct Timestamp {
seconds: u64,
nanos: u32,
}
// For deserialization: accept human-readable ISO format
#[derive(Deserialize)]
struct IsoTime(String);
// For serialization: output Unix timestamp
#[derive(Serialize)]
struct UnixTime {
seconds: u64,
nanos: u32,
}
#[derive(Serialize, Deserialize)]
#[serde(
from = "IsoTime", // Deserialize from ISO string
into = "UnixTime" // Serialize as Unix timestamp
)]
struct DateTime(Timestamp);
impl From<IsoTime> for DateTime {
fn from(iso: IsoTime) -> Self {
// Parse ISO format to timestamp
// Simplified example
DateTime(Timestamp { seconds: 1609459200, nanos: 0 })
}
}
impl From<DateTime> for UnixTime {
fn from(dt: DateTime) -> Self {
UnixTime {
seconds: dt.0.seconds,
nanos: dt.0.nanos,
}
}
}
fn main() {
// Deserialize from ISO format
let dt: DateTime = serde_json::from_str("\"2021-01-01T00:00:00Z\"").unwrap();
// Serialize as Unix timestamp
let json = serde_json::to_string(&dt).unwrap();
println!("As Unix: {}", json); // {"seconds":1609459200,"nanos":0}
}from and into can specify different types for asymmetric conversion.
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
// Problem: Can't implement Deserialize for SystemTime directly
// because SystemTime is from std and Serialize/Deserialize are foreign
// Solution: Use a proxy type
#[derive(Serialize, Deserialize)]
#[serde(from = "UnixTimestamp", into = "UnixTimestamp")]
struct SystemTimeProxy(SystemTime);
#[derive(Serialize, Deserialize)]
struct UnixTimestamp {
secs_since_epoch: u64,
nanos: u32,
}
impl From<UnixTimestamp> for SystemTimeProxy {
fn from(ts: UnixTimestamp) -> Self {
let duration = std::time::Duration::new(ts.secs_since_epoch, ts.nanos);
SystemTimeProxy(SystemTime::UNIX_EPOCH + duration)
}
}
impl From<SystemTimeProxy> for UnixTimestamp {
fn from(st: SystemTimeProxy) -> Self {
let duration = st.0.duration_since(SystemTime::UNIX_EPOCH).unwrap();
UnixTimestamp {
secs_since_epoch: duration.as_secs(),
nanos: duration.subsec_nanos(),
}
}
}
fn main() {
let now = SystemTimeProxy(SystemTime::now());
let json = serde_json::to_string(&now).unwrap();
println!("Serialized: {}", json);
let restored: SystemTimeProxy = serde_json::from_str(&json).unwrap();
println!("Restored successfully");
}The conversion pattern works around orphan rule limitations by using local types.
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
#[derive(Debug)]
struct CaseInsensitiveString(String);
#[derive(Deserialize)]
#[serde(from = "Cow<'_, str>")] // Can borrow from input
struct CaseInsensitiveBorrowed(CaseInsensitiveString);
impl From<Cow<'_, str>> for CaseInsensitiveBorrowed {
fn from(s: Cow<'_, str>) -> Self {
CaseInsensitiveBorrowed(CaseInsensitiveString(s.into_owned().to_lowercase()))
}
}
fn main() {
let json = r#""Hello World""#;
let result: CaseInsensitiveBorrowed = serde_json::from_str(json).unwrap();
println!("Lowercased: {}", result.0 .0); // "hello world"
}Using Cow in the intermediate type allows zero-copy deserialization when possible.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug)]
struct AppConfig {
max_connections: usize,
timeout_ms: u64,
debug: bool,
}
#[derive(Serialize, Deserialize)]
#[serde(from = "HashMap<String, String>", into = "HashMap<String, String>")]
struct Config(AppConfig);
impl From<HashMap<String, String>> for Config {
fn from(map: HashMap<String, String>) -> Self {
Config(AppConfig {
max_connections: map.get("max_connections")
.and_then(|s| s.parse().ok())
.unwrap_or(100),
timeout_ms: map.get("timeout_ms")
.and_then(|s| s.parse().ok())
.unwrap_or(5000),
debug: map.get("debug")
.map(|s| s == "true")
.unwrap_or(false),
})
}
}
impl From<Config> for HashMap<String, String> {
fn from(config: Config) -> Self {
let mut map = HashMap::new();
map.insert("max_connections".to_string(), config.0.max_connections.to_string());
map.insert("timeout_ms".to_string(), config.0.timeout_ms.to_string());
map.insert("debug".to_string(), config.0.debug.to_string());
map
}
}
fn main() {
let json = r#"{"max_connections":"200","timeout_ms":"10000","debug":"true"}"#;
let config: Config = serde_json::from_str(json).unwrap();
println!("Max conn: {}, debug: {}", config.0.max_connections, config.0.debug);
let serialized = serde_json::to_string(&config).unwrap();
println!("Back to JSON: {}", serialized);
}The intermediate type can be any serializable type, not just primitives.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
struct Email(String);
#[derive(Serialize, Deserialize)]
struct User {
name: String,
#[serde(from = "String", into = "String")]
email: Email,
}
impl From<String> for Email {
fn from(s: String) -> Self {
// Could validate email here
Email(s)
}
}
impl From<Email> for String {
fn from(email: Email) -> Self {
email.0
}
}
fn main() {
let user = User {
name: "Alice".to_string(),
email: Email("alice@example.com".to_string()),
};
let json = serde_json::to_string(&user).unwrap();
println!("User JSON: {}", json);
let parsed: User = serde_json::from_str(&json).unwrap();
println!("Email: {}", parsed.email.0);
}The from/into attributes can be applied to individual fields.
use serde::{Deserialize, Serialize};
// Using from/into - conversion must succeed
#[derive(Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
struct Port(u16);
impl From<String> for Port {
fn from(s: String) -> Self {
Port(s.parse().unwrap_or(80)) // Fallback on error
}
}
impl From<Port> for String {
fn from(p: Port) -> Self {
p.0.to_string()
}
}
// If you need fallible conversion, you need a custom Deserialize impl
// because From/Into are infallible
fn main() {
let port: Port = serde_json::from_str("\"invalid\"").unwrap();
println!("Port: {}", port.0); // Falls back to 80
}from/into conversions are infallible; use custom implementations for fallible conversions.
use serde::{Deserialize, Serialize};
#[derive(Debug)]
struct Kilometers(f64);
#[derive(Debug)]
struct Miles(f64);
#[derive(Serialize, Deserialize)]
enum Distance {
#[serde(from = "f64", into = "f64")]
Kilometers(f64),
#[serde(from = "f64", into = "f64")]
Miles(f64),
}
impl From<f64> for Kilometers {
fn from(v: f64) -> Self {
Kilometers(v)
}
}
impl From<Kilometers> for f64 {
fn from(k: Kilometers) -> Self {
k.0
}
}
impl From<f64> for Miles {
fn from(v: f64) -> Self {
Miles(v)
}
}
impl From<Miles> for f64 {
fn from(m: Miles) -> Self {
m.0
}
}
fn main() {
let km = Distance::Kilometers(10.0);
let json = serde_json::to_string(&km).unwrap();
println!("Serialized: {}", json);
let parsed: Distance = serde_json::from_str(&json).unwrap();
println!("Deserialized successfully");
}Each enum variant can have its own conversion type.
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug)]
struct SafePath(PathBuf);
#[derive(Serialize, Deserialize)]
#[serde(from = "String", into = "String")]
struct SafePathWrapper(SafePath);
impl From<String> for SafePathWrapper {
fn from(s: String) -> Self {
// Sanitize path: remove .. and other dangerous patterns
let safe = s.replace("..", "");
SafePathWrapper(SafePath(PathBuf::from(safe)))
}
}
impl From<SafePathWrapper> for String {
fn from(p: SafePathWrapper) -> Self {
p.0 .0.to_string_lossy().into_owned()
}
}
fn main() {
let json = r#""../../../etc/passwd""#;
let safe: SafePathWrapper = serde_json::from_str(json).unwrap();
println!("Sanitized path: {:?}", safe.0 .0);
}Conversions can include validation and sanitization logic.
use serde::{Deserialize, Serialize, Serializer, Deserializer};
// Using from/into attribute
#[derive(Serialize, Deserialize)]
#[serde(from = "u32", into = "u32")]
struct IdAttribute(u32);
impl From<u32> for IdAttribute {
fn from(id: u32) -> Self {
IdAttribute(id)
}
}
impl From<IdAttribute> for u32 {
fn from(id: IdAttribute) -> Self {
id.0
}
}
// Manual implementation (more verbose)
struct IdManual(u32);
impl<'de> Deserialize<'de> for IdManual {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = u32::deserialize(deserializer)?;
Ok(IdManual(value))
}
}
impl Serialize for IdManual {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
fn main() {
let attr = IdAttribute(42);
let json = serde_json::to_string(&attr).unwrap();
println!("Attribute: {}", json);
let manual = IdManual(42);
let json = serde_json::to_string(&manual).unwrap();
println!("Manual: {}", json);
}The attribute approach is more concise than manual trait implementations.
use serde::{Deserialize, Serialize};
// serde also supports try_from for fallible conversions
#[derive(Debug)]
struct Positive(i32);
#[derive(Deserialize)]
#[serde(try_from = "i32")] // Fallible conversion
struct PositiveDeserialize(Positive);
impl TryFrom<i32> for PositiveDeserialize {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value > 0 {
Ok(PositiveDeserialize(Positive(value)))
} else {
Err(format!("Value {} is not positive", value))
}
}
}
fn main() {
let good: Result<PositiveDeserialize, _> = serde_json::from_str("42");
println!("Good: {:?}", good);
let bad: Result<PositiveDeserialize, _> = serde_json::from_str("-5");
println!("Bad: {:?}", bad);
}Use try_from when the conversion can fail.
| Attribute | Purpose | Direction |
|-----------|---------|-----------|
| from = "T" | Deserialize via T | Input → T → Self |
| into = "T" | Serialize via T | Self → T → Output |
| try_from = "T" | Fallible deserialize | Input → T → Result |
The #[serde(from = "...", into = "...")] attribute provides a declarative conversion layer:
The conversion pipeline: During deserialization, serde reads input as the from type, then calls From<from_type>. During serialization, serde calls From<Self> to get the into type, then serializes that. This two-step process lets you express transformations that direct Serialize/Deserialize implementations cannot, because the intermediate type can be any serializable form.
Orphan rule workaround: The pattern's most common use is implementing serialization for types you don't own. Since Serialize and Deserialize are foreign traits, you can't implement them for SystemTime directly. But you can create a local wrapper with from/into pointing to a serializable proxy type you define.
Asymmetric representations: Using different types for from and into enables input/output format differences. Read human-readable ISO timestamps, write machine-efficient Unix numbers. Accept flexible string formats, output normalized forms.
Key insight: The attribute is a code generation directive. Serde's derive macros read these attributes and generate Serialize/Deserialize implementations that call your From implementations. You write the conversion logic once, and serde handles the boilerplate. The conversion is always infallible from serde's perspective—if validation can fail, use try_from or implement Deserialize manually.
The pattern works best when your type's serialized form differs from its in-memory representation but can be transformed through straightforward conversion. For complex validation, conditional serialization, or context-dependent behavior, manual Serialize/Deserialize implementations remain more appropriate.