Mục lục
- Mục Tiêu Bài Học
- Khi Nào Cần Custom Serializer/Deserializer
serialize_with/deserialize_with— Mức 2 Recap- Manual
impl Serialize— Serializer Trait - Manual
impl Deserialize+ Visitor Pattern expectingMethod — Error Message Cho Người Đọc- Apply: Refactor
PaymentMethod::Cod+UserResponseDto - So Sánh 3 Mức Custom
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu khi nào
#[derive(Serialize, Deserialize)]KHÔNG đủ, cần manual impl trait. - Phân biệt 3 cách custom:
deserialize_withfunction per-field,serialize_withfunction per-field, manualimpl Serialize/Deserializecho whole type. - Hiểu Visitor pattern — cách serde traverse format không biết trước (JSON, YAML, TOML, MsgPack).
- Implement
Phonenewtype tự normalize format0912...↔+84912...về wire chuẩn quốc tế. - Phân biệt
Deserializer::deserialize_str(gợi ý format cho parser) vs Visitorvisit_str(callback nhận string). - Áp
PhonechoPaymentMethod::Cod(B43 refactor) vàUserResponseDto(B45 refactor) — wire format consistent+84xxxxxxxxx.
Khi Nào Cần Custom Serializer/Deserializer
serde cho phép tùy biến ở 3 mức từ đơn giản → phức tạp:
Mức | Tên | Phạm vi | Effort
1 | #[derive(Serialize, Deserialize)] | Whole type | Low
2 | #[serde(serialize_with / deserialize_with)] | Per-field | Medium
3 | Manual impl Serialize / impl Deserialize | Whole type | High
Mức 1 đã đủ cho 95% type Rust standard — String, i64, DateTime<Utc> (với feature serde), struct DTO, enum tagged. Mức 2 đã preview ở B42 với helper deserialize_optional_field cho double-Option PATCH semantic.
Mức 3 cần khi 1 trong 4 case:
- Type third-party không có derive macro — wrap struct external crate KHÔNG impl
Serialize, phải tự implement. - Cần handle nhiều format input linh hoạt — field có thể nhận cả string lẫn số (vd ID đến từ JSON
"123"string format hoặc123number format). - Normalize / transform value trong quá trình deserialize — input phong phú, output chuẩn hóa.
- Newtype có rule validate phức tạp hơn validator crate hỗ trợ — vd state machine, parse format đặc thù.
Use case Shop API B46: Phone field nhận 3 format input từ client:
// Client A gửi format số 0 prefix (phổ biến VN):
{"phone": "0912345678"}
// Client B gửi format quốc tế đầy đủ (chuẩn E.164):
{"phone": "+84912345678"}
// Client C gửi format không có dấu + (SMS gateway thường):
{"phone": "84912345678"}
Cả 3 đều hợp lệ về mặt nghiệp vụ. Server cần normalize về 1 format chuẩn quốc tế +84912345678 để DB lưu nhất quán, đối soát với SMS gateway / Stripe / VietQR không miss-match format. Validator crate B41 chỉ check regex (PHONE_REGEX ^(0|\+84)(\d{9,10})$ lock B43) nhưng KHÔNG transform — sau validate vẫn giữ format gốc, code downstream phải normalize thủ công ở nhiều chỗ.
Giải pháp gọn: tạo Phone newtype với manual impl Serialize/Deserialize — deserialize tự normalize, serialize xuất raw normalized; mọi nơi dùng Phone tự động có invariant đúng format.
serialize_with / deserialize_with — Mức 2 Recap
Trước khi nhảy thẳng mức 3, xem mức 2 đã làm được gì cho use case Phone. Mức 2 gắn function path vào field qua attribute:
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Serialize, Deserialize)]
pub struct UserDto {
#[serde(serialize_with = "serialize_phone")]
#[serde(deserialize_with = "deserialize_phone")]
pub phone: String,
}
fn serialize_phone<S>(phone: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let normalized = normalize_phone(phone).unwrap_or_else(|_| phone.to_string());
serializer.serialize_str(&normalized)
}
fn deserialize_phone<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
normalize_phone(&s).map_err(serde::de::Error::custom)
}
fn normalize_phone(raw: &str) -> Result<String, String> {
// logic normalize ...
Ok(raw.to_string())
}
Cách này hoạt động nhưng có 3 nhược điểm:
- Mỗi field phải lặp 2 attribute —
UserResponseDto.phone,PaymentMethod::Cod.phone,AddressDto.phonetương lai đều phải gắn lại — dễ quên sót 1 nơi, format không đồng nhất. - Type vẫn là
String— mất type safety; hàm nhậnfn send_sms(to: &str)không phân biệt được "đây là phone đã normalize" hay "đây là user input chưa normalize". - Code repetition — function path string KHÔNG type-check, đổi tên hàm phải sửa mọi attribute string.
Giải pháp: gói invariant vào type — tạo Phone newtype với impl Serialize/Deserialize riêng. Mức 3 này giải quyết cả 3 nhược điểm trên cùng lúc.
Manual impl Serialize — Serializer Trait
Serializer trait có method tương ứng với mỗi JSON primitive: serialize_str (string), serialize_i64 (signed integer), serialize_u64 (unsigned), serialize_bool (boolean), serialize_seq (array), serialize_map (object), serialize_none/serialize_some (null/value). Impl Serialize cho type bằng cách gọi method phù hợp.
Define Phone newtype với inner String private trong crates/shop-common/src/dto/types.rs (extend B44):
// File: crates/shop-common/src/dto/types.rs (extend B44)
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::de::{self, Visitor};
use std::fmt;
use thiserror::Error;
/// Số điện thoại VN đã normalize về format quốc tế `+84xxxxxxxxx`.
///
/// Inner String **private** đảm bảo invariant: mọi instance đều normalized.
/// Constructor `Phone::new()` là cách duy nhất tạo `Phone` từ raw input.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Phone(String); // ← private inner
impl Phone {
/// Parse + normalize 3 format input về `+84xxxxxxxxx`.
pub fn new(raw: &str) -> Result<Self, PhoneError> {
let normalized = normalize_vietnam_phone(raw)?;
Ok(Phone(normalized))
}
/// Borrow inner string đã normalized.
pub fn as_str(&self) -> &str {
&self.0
}
/// Tiêu thụ Phone trả String inner (vd khi cần dạng owned vào DB).
pub fn into_inner(self) -> String {
self.0
}
}
#[derive(Debug, Clone, Error)]
pub enum PhoneError {
#[error("phone number invalid: {0}")]
Invalid(String),
}
Helper private normalize_vietnam_phone cùng module xử lý 3 format input:
fn normalize_vietnam_phone(raw: &str) -> Result<String, PhoneError> {
// Strip khoảng trắng + dấu gạch nối user thường gõ (vd "0912-345-678")
let cleaned: String = raw
.chars()
.filter(|c| c.is_ascii_digit() || *c == '+')
.collect();
let normalized = if let Some(rest) = cleaned.strip_prefix("+84") {
format!("+84{}", rest)
} else if let Some(rest) = cleaned.strip_prefix("84") {
format!("+84{}", rest)
} else if let Some(rest) = cleaned.strip_prefix('0') {
format!("+84{}", rest)
} else {
return Err(PhoneError::Invalid(raw.to_string()));
};
// Validate length sau normalize: +84 + 9 chữ số = 12 ký tự total
if normalized.len() != 12 {
return Err(PhoneError::Invalid(raw.to_string()));
}
Ok(normalized)
}
Impl Serialize 1 dòng — xuất raw string đã normalized:
impl Serialize for Phone {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.0)
}
}
Trade-off: Serializer generic S nên 1 impl chạy cho mọi format (JSON, YAML, TOML, MsgPack) — KHÔNG cần viết riêng cho từng format. Method serialize_str là interface chuẩn, mỗi format implement riêng cách encode string đó (JSON quote escape, YAML literal, MsgPack length-prefix).
Test serialize:
let phone = Phone::new("0912345678").unwrap();
let json = serde_json::to_string(&phone).unwrap();
assert_eq!(json, "\"+84912345678\""); // wire format chuẩn
Manual impl Deserialize + Visitor Pattern
Deserialize phức tạp hơn vì Deserializer KHÔNG biết format input đang là gì (JSON, YAML, TOML, ...) ở thời điểm runtime. Mỗi format encode value khác nhau:
- JSON encode string
"0912345678"với double-quote escape. - YAML encode
0912345678không quote nếu là plain scalar. - MsgPack encode binary length-prefix.
serde giải bài toán này bằng Visitor pattern — bạn cung cấp 1 struct Visitor với method visit_str/visit_i64/visit_map/... mỗi method handle 1 loại value. Format deserializer khi parse được value sẽ gọi method phù hợp với type gặp.
Impl Deserialize cho Phone chỉ 1 dòng — gọi deserializer.deserialize_str(PhoneVisitor):
impl<'de> Deserialize<'de> for Phone {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(PhoneVisitor)
}
}
struct PhoneVisitor;
impl<'de> Visitor<'de> for PhoneVisitor {
type Value = Phone;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a Vietnamese phone number string (0xxx, 84xxx, or +84xxx)")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Phone::new(value).map_err(de::Error::custom)
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
Phone::new(&value).map_err(de::Error::custom)
}
}
Workflow deserialize khi JSON gửi {"phone": "0912345678"}:
1. serde_json parse value `"0912345678"` (JSON string)
2. serde gọi `Phone::deserialize(json_deserializer)`
3. Phone gọi `json_deserializer.deserialize_str(PhoneVisitor)`
→ gợi ý cho JSON parser: "tôi expect string đây"
4. JSON deserializer extract &str raw từ buffer
→ gọi `visitor.visit_str("0912345678")`
5. Visitor gọi `Phone::new("0912345678")` → normalize → Phone("+84912345678")
6. Return Phone successful, serde tiếp tục deserialize field khác
Phân biệt quan trọng:
Deserializer::deserialize_str— bạn nói với format parser "tôi expect 1 string". Format optimize parse (vd JSON skip{/[prefix scan, đi thẳng vào string parsing).- Visitor
visit_str— format parser gọi ngược lại bạn với value đã parse: "đây là string tôi parse được, xử lý đi".
Hai chiều khác nhau — deserialize_* là hint cho format, visit_* là callback nhận value.
Tại sao cần cả visit_str + visit_string:
visit_str(&str)— nhận borrow; JSON deserializer (serde_json) thường gọi method này khi string nằm trong buffer parse, KHÔNG phải allocateStringmới.visit_string(String)— nhận owned; một số format (vd serde_yaml với escape sequence, hoặc lazy deserializer) tự allocateStringrồi pass owned vào.
Implement cả 2 method giúp Phone hoạt động với mọi format serde-aware. Trace code: cả 2 đều delegate cho Phone::new() normalize — chỉ khác signature input.
Test deserialize 3 format input:
let p1: Phone = serde_json::from_str("\"0912345678\"").unwrap();
let p2: Phone = serde_json::from_str("\"+84912345678\"").unwrap();
let p3: Phone = serde_json::from_str("\"84912345678\"").unwrap();
assert_eq!(p1.as_str(), "+84912345678");
assert_eq!(p2.as_str(), "+84912345678");
assert_eq!(p3.as_str(), "+84912345678");
// → cả 3 format đều normalize về wire chuẩn quốc tế
Visitor pattern là industry standard — mọi crate serde-aware (sqlx, validator, jsonwebtoken, ...) đều theo. Hiểu pattern này giúp đọc source code và implement deserialize cho mọi type custom Shop API tương lai.
expecting Method — Error Message Cho Người Đọc
Method expecting trong Visitor báo cho serde "type này expect input dạng gì" — dùng cho error message khi value gặp KHÔNG match Visitor method nào:
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a Vietnamese phone number string (0xxx, 84xxx, or +84xxx)")
}
Khi client gửi {"phone": 12345} (số thay vì string), workflow:
1. JSON parse value 12345 → integer i64
2. serde gọi `Phone::deserialize(json_de)`
3. Phone gọi `json_de.deserialize_str(PhoneVisitor)`
4. JSON deserializer phát hiện current value là integer, không phải string
→ KHÔNG gọi `visit_str` hay `visit_string`
→ fallback gọi `visit_i64(12345)` — nhưng PhoneVisitor không impl method này
→ default impl trong Visitor trait trả error với content từ `expecting()`
5. Error message return cho client:
"invalid type: integer `12345`, expected a Vietnamese phone number string
(0xxx, 84xxx, or +84xxx)"
Message rõ ràng "expected XXX, got YYY" giúp client debug nhanh — không cần đọc source server. Pattern lock Shop API: expecting luôn viết rõ format chấp nhận, có ví dụ cụ thể trong dấu ngoặc, KHÔNG generic "a phone" không actionable.
So sánh 2 phong cách expecting:
// Tốt — actionable, có ví dụ format
formatter.write_str("a Vietnamese phone number string (0xxx, 84xxx, or +84xxx)")
// Xấu — generic, client không biết format gì hợp lệ
formatter.write_str("a phone")
Error message rõ format chấp nhận cũng phù hợp error envelope chuẩn Shop API lock B16 — khi extractor trả 400 BAD_REQUEST, body envelope {"error": "...", "code": "BAD_REQUEST", "request_id": "..."} chứa luôn nội dung expecting giúp debug nhanh ở dev tool browser hoặc log monitoring.
Apply: Refactor PaymentMethod::Cod + UserResponseDto
Áp Phone newtype vào 2 nơi đã define ở B43 (PaymentMethod) và B45 (User). Update crates/shop-common/src/dto/mod.rs re-export top-level:
// File: crates/shop-common/src/dto/mod.rs (snippet — chỉ phần B46 thêm)
pub mod payment;
pub mod product;
pub mod types;
pub mod user;
pub use payment::PaymentMethod;
pub use product::{CreateProductDto, SLUG_REGEX, UpdateProductDto};
pub use types::{CategoryId, Money, OrderId, Phone, PhoneError, ProductId, UserId};
pub use user::{User, UserResponseDto};
Refactor crates/shop-common/src/dto/payment.rs — variant Cod đổi type phone: String sang phone: Phone:
// File: crates/shop-common/src/dto/payment.rs (refactor B46)
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::dto::Phone; // ← import Phone từ dto/types.rs
#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PaymentMethod {
Stripe {
#[validate(length(min = 10, max = 200))]
payment_intent_id: String,
#[validate(length(min = 10, max = 200))]
customer_id: String,
},
BankTransfer {
#[validate(length(min = 2, max = 100))]
bank_name: String,
#[validate(regex(path = *super::ACCOUNT_NUMBER_REGEX))]
account_number: String,
},
Cod {
phone: Phone, // ← Phone newtype thay String + validate regex
},
}
Note: validator crate trên field phone bị bỏ vì validate đã chạy trong Deserialize của Phone (fail fast ở extract trước cả khi ValidatedJson::from_request gọi value.validate()?). PHONE_REGEX constant B43 vẫn lock ở dto/mod.rs cho future use case String field (vd AddressDto.phone nếu không muốn dùng Phone newtype).
Refactor crates/shop-common/src/dto/user.rs — field phone: Option<String> đổi sang Option<Phone>:
// File: crates/shop-common/src/dto/user.rs (refactor B46)
use chrono::{DateTime, Utc};
use serde::Serialize;
use super::{Phone, UserId};
#[derive(Debug, Clone, Serialize)]
pub struct UserResponseDto {
pub id: UserId,
pub email: String,
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone: Option<Phone>, // ← Phone newtype thay String
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_login_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone)]
pub struct User {
pub id: UserId,
pub email: String,
pub password_hash: String,
pub display_name: String,
pub avatar_url: Option<String>,
pub phone: Option<String>, // ← DB cột TEXT, vẫn String raw
pub created_at: DateTime<Utc>,
pub last_login_at: Option<DateTime<Utc>>,
pub deleted_at: Option<DateTime<Utc>>,
}
impl From<User> for UserResponseDto {
fn from(user: User) -> Self {
Self {
id: user.id,
email: user.email,
display_name: user.display_name,
avatar_url: user.avatar_url,
// Parse từ String DB qua Phone::new; drop invalid silent (DB load không fail)
phone: user.phone.and_then(|p| Phone::new(&p).ok()),
created_at: user.created_at,
last_login_at: user.last_login_at,
}
}
}
Note quan trọng:
User.phonegiữOption<String>raw — đại diện cột DBTEXT NULL, có thể chứa data lịch sử chưa normalize từ migrate cũ.From<User> for UserResponseDtoparse quaPhone::new(&p).ok()— invalid phone bị drop silent ở response (không throw error, không leak data sai format ra wire).- Endpoint write (B104 register, PATCH profile) sẽ dùng
Phonetrong input DTO — fail fast ở extract nếu user gửi phone invalid.
JSON wire input client gửi 3 format (Cod):
// Client A — format số 0 prefix:
{"type": "cod", "phone": "0912345678"}
// Client B — format quốc tế đầy đủ:
{"type": "cod", "phone": "+84912345678"}
// Client C — format không có dấu +:
{"type": "cod", "phone": "84912345678"}
Cả 3 đều được accept, server normalize về 1 format. Response server trả luôn format chuẩn quốc tế:
{
"type": "cod",
"phone": "+84912345678"
}
Error khi phone invalid (client gửi "phone": "ABC123"):
{
"error": "invalid JSON: phone number invalid: ABC123",
"code": "BAD_REQUEST",
"request_id": ""
}
HTTP status 400 — lock AppJson extractor B32 map JsonDataError sang AppError::BadRequest qua envelope chuẩn.
Verify compile sau refactor:
cd shop && cargo build -p shop-common
# Output:
# Compiling shop-common v0.1.0
# Finished `dev` profile [unoptimized + debuginfo] target(s)
Suggested commit:
git add crates/shop-common/src/dto/types.rs \
crates/shop-common/src/dto/mod.rs \
crates/shop-common/src/dto/payment.rs \
crates/shop-common/src/dto/user.rs
git commit -m "B46: Phone newtype + Visitor pattern + refactor PaymentMethod::Cod + UserResponseDto"
So Sánh 3 Mức Custom
Bảng decision matrix lock Shop API:
Mức | Use case | Effort | Type safety
1 derive| Type Rust standard, struct DTO | Low | Yes
2 fn | 1-2 field cần transform 1-off | Medium | No (vẫn primitive)
3 impl | Newtype có rule normalize phức tạp | High | Yes (type level)
Shop API decision lock vĩnh viễn:
- Mức 1 cho 95% type — ID newtype (UserId, ProductId, OrderId, CategoryId B44), struct DTO (CreateProductDto B41/B44, UpdateProductDto B42, UserResponseDto B45), enum tagged (PaymentMethod B43).
- Mức 2 cho transform 1-off field — helper
deserialize_optional_fieldB42 cho double-Option PATCH semantic; field tương tự sau này dùngdeserialize_with = "fn_path". - Mức 3 cho newtype có rule normalize —
PhoneB46 (normalize 3 format VN),MoneyB54 sẽ refactor (Decimal precision validate),EmailB105 sẽ wrap (lowercase normalize + DNS check optional), futureUrlwrap (parse scheme + host validate).
Pattern: KHÔNG over-engineer — chỉ lên mức cao khi rule không thể derive được. Mức 1 đơn giản, dễ đọc — ưu tiên dùng đến hết khả năng trước khi nhảy lên mức 2 hoặc 3.
Visitor lib có sẵn — crate serde_with cung cấp pre-built Visitor cho format phổ biến (URL, IP, Duration, base64, hex, comma-separated list). Đáng tham khảo nếu nhiều field cần transform format chuẩn — tránh viết lại Visitor cho mỗi loại. Shop API hiện chưa add serde_with vì Phone đã đủ cho rule VN-specific, nhưng B53 (chrono DateTime custom format) sẽ cân nhắc add khi cần parse multi-format datetime input.
Tổng Kết
- 3 mức custom serde: derive (mức 1),
withfn per-field (mức 2), manual impl + Visitor (mức 3) — pick theo độ phức tạp rule. - Visitor pattern chuẩn industry — serde traverse format không biết trước (JSON, YAML, TOML, MsgPack) qua method
visit_*cho mỗi primitive. visit_str+visit_stringcùng impl — handle JSON borrow (&str) + format khác owned (String) cho cross-format flexibility.expectingmethod quan trọng cho error message — luôn ghi rõ format chấp nhận có ví dụ cụ thể, KHÔNG generic.- Phân biệt
Deserializer::deserialize_strvs Visitorvisit_str— đầu là hint cho format parser, sau là callback nhận value. Phonenewtype Shop API lock — normalize 3 format VN (0xxx/84xxx/+84xxx) về wire chuẩn quốc tế+84xxxxxxxxx.- Inner String private (
Phone(String)KHÔNG(pub String)) — đảm bảo invariant normalized chỉ qua constructorPhone::new(). - Constructor
Phone::new()Result-returning + accessoras_str()+into_inner()— ergonomic API thayFrom<String>infallible (parse có thể fail). - Validate trong Deserialize sớm hơn validator crate — fail fast ở extract trước khi handler chạy.
- Apply pattern:
PaymentMethod::Cod(B43 refactor) +UserResponseDto.phone(B45 refactor) + futureCreateUserDtoB104,AddressDtoB105. - File path lock:
dto/types.rsextend (B44 5 newtype + B46Phone+PhoneError+normalize_vietnam_phoneprivate). From<User> for UserResponseDtohandle parse — String DB quaPhone::new(&p).ok(), drop invalid silent (DB load không fail).- Phone PARTIAL replace PHONE_REGEX B43 — PaymentMethod::Cod dùng Phone, regex constant vẫn lock cho future String field.
- Foundation cho B47 (Vec, HashMap collection trong JSON — size limit chống DoS), B53 (chrono DateTime custom format multi-input), B105 (Email newtype wrap String).
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- 3 mức custom serde khác nhau ra sao? Cho ví dụ use case mỗi mức và lý do chọn (effort vs type safety trade-off).
- Visitor pattern là gì? Tại sao cần
visit_str+visit_stringcùng impl? Format nào gọi method nào? expectingmethod dùng để làm gì? Cho ví dụ error message tốt vs xấu khi client gửi{"phone": 12345}.- Tại sao
Phonedùng inner private (Phone(String)) thay(pub String)? Lợi ích vs trade-off ergonomic. - Khác nhau giữa
Deserializer::deserialize_strvà Visitorvisit_str? Liên hệ pattern Rust traverse type không biết format.
Đáp án
- 3 mức custom serde — use case + trade-off. Mức 1
#[derive(Serialize, Deserialize)]: dùng cho type Rust standard có sẵn impl trait (String,i64,DateTime<Utc>với feature serde), struct DTO field đơn giản, enum tagged. Effort thấp (1 dòng attribute), type safety tốt (struct riêng), lock cho 95% case Shop API: ID newtype, CreateProductDto B41, UpdateProductDto B42, PaymentMethod B43, UserResponseDto B45.
Mức 2#[derive(Serialize, Deserialize)] pub struct UserDto { pub id: u64, pub name: String, }#[serde(serialize_with / deserialize_with = "fn_name")]: dùng cho 1-2 field cần transform 1-off, type không cần đổi (vẫn primitive). Effort vừa (1 function path string per field per direction), type safety KHÔNG (vẫn primitive nên dev khác không phân biệt được "đã transform" vs "raw"); pitfall function path string không type-check, đổi tên hàm phải sửa attribute. Lock Shop API: helperdeserialize_optional_fieldB42 cho double-Option PATCH.
Mức 3 manual#[derive(Serialize, Deserialize)] pub struct UpdateDto { #[serde(default, deserialize_with = "deserialize_optional_field")] pub description: Option<Option<String>>, }impl Serialize+impl Deserialize: dùng cho newtype có rule normalize / validate phức tạp (Phone B46, Email B105, Money refactor B54 với Decimal precision), type third-party không có derive macro. Effort cao (Visitor pattern + 4 method MANDATORYtype Value/expecting/visit_str/visit_string), type safety tốt nhất (type level, mọi nơi dùng tự động có invariant). Lock Shop API: chỉ lên mức 3 khi rule không derive được — KHÔNG over-engineer.
Quy tắc chọn: type Rust standard → mức 1; 1-off transform field cần normalize không đổi type → mức 2; newtype với invariant phức tạp (normalize, validate complex) → mức 3. Phone case Shop API: mức 1 (impl Serialize for Phone { fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> { ser.serialize_str(&self.0) } }#[serde(transparent)]) chỉ wire raw string, KHÔNG normalize; mức 2 lặp attribute cho mỗi field phone, type vẫn String mất safety; mức 3 đúng — Phone newtype tự normalize ở deserialize, mọi nơi dùng tự động có invariant. - Visitor pattern + visit_str vs visit_string. Visitor pattern = cách serde traverse format input KHÔNG biết trước (JSON, YAML, TOML, MsgPack). Mỗi format encode value khác nhau (JSON quote escape, YAML literal, MsgPack length-prefix); serde không bake hardcode 1 format vào Deserialize impl của user. Giải pháp: user cung cấp 1 struct
Visitorvới methodvisit_str/visit_i64/visit_u64/visit_bool/visit_map/visit_seq/... mỗi method handle 1 loại JSON primitive; format deserializer khi parse được value gọi method phù hợp với type gặp. Visitor có 4 phần MANDATORY:type Value = Output(output type),expecting(báo format expect cho error message), 1+ methodvisit_*cho input cần handle.
Tại sao cần cả 2 method:impl<'de> Visitor<'de> for PhoneVisitor { type Value = Phone; fn expecting(&self, f: &mut Formatter) -> fmt::Result { f.write_str("a Vietnamese phone number string (0xxx, 84xxx, or +84xxx)") } fn visit_str<E: de::Error>(self, v: &str) -> Result<Phone, E> { Phone::new(v).map_err(de::Error::custom) } fn visit_string<E: de::Error>(self, v: String) -> Result<Phone, E> { Phone::new(&v).map_err(de::Error::custom) } }visit_str(&str)nhận borrow string từ buffer parse — JSON deserializer (serde_json) thường gọi method này khi string nằm thẳng trong buffer không cần allocateStringmới (zero-copy).visit_string(String)nhận owned string — một số format (serde_yaml với escape sequence cần allocate buffer mới, lazy deserializer pre-build String) gọi method này. Format nào gọi method nào: JSON đơn giản (không escape) →visit_strborrow zero-copy; JSON với escape sequence ("abc\nxyz") → một số impl gọivisit_stringowned vì cần buffer rebuild; YAML/TOML tùy parser. Implement cả 2 method là pattern lock — flexibility cross-format, fallback method có default impl trả error nếu không impl. Code Phone case: 2 method đều delegatePhone::new()— chỉ khác signature input (borrow vs owned). Visitor pattern là industry standard — sqlx, validator, jsonwebtoken, mọi crate serde-aware đều theo. expectingmethod + error message tốt vs xấu. Mục đích: báo cho serde "type này expect input dạng gì" — dùng cho error message khi value gặp KHÔNG match Visitor method nào. Default impl mọivisit_*KHÔNG override trả error với content từexpecting(). Workflow khi client gửi{"phone": 12345}(integer thay string): (a) JSON parse value12345→ integer i64; (b) serde gọiPhone::deserialize(json_de)→json_de.deserialize_str(PhoneVisitor)(gợi ý format string cho parser); (c) JSON deserializer phát hiện current value là integer không phải string, KHÔNG gọivisit_str/visit_string; (d) fallback gọivisit_i64(12345)— PhoneVisitor không impl method này nên default trả error với contentexpecting(). Error tốt (Phone case):
Client đọc message biết ngay: (i) lỗi type sai (integer thay string), (ii) format expect cụ thể (0xxx / 84xxx / +84xxx) với 3 ví dụ, (iii) vị trí lỗi (line 1 column 18) trong JSON body. Có thể fix client ngay không cần đọc source server. Error xấu (genericinvalid type: integer `12345`, expected a Vietnamese phone number string (0xxx, 84xxx, or +84xxx) at line 1 column 18expecting):
Client biết phone type sai nhưng KHÔNG biết format gì hợp lệ — phải đọc OpenAPI doc, source code, hoặc trial-error mới biết. Tăng support ticket + frustration. Pattern lock Shop API:invalid type: integer `12345`, expected a phone at line 1 column 18expectingluôn (i) chỉ rõ kind (string/integer/object/...), (ii) có ví dụ cụ thể trong dấu ngoặc, (iii) KHÔNG generic noun như "phone"/"user"/"id" không actionable. Áp cho mọi Visitor Shop API tương lai — Email B105 "a valid email address string ([email protected])", Money B54 "a decimal number string (\"25000.00\")", Url wrap "a valid HTTPS URL (https://example.com)". Lợi ích Shop API integration với envelope error chuẩn B16: extractorAppJsonmapJsonDataErrorsangAppError::BadRequest, envelope{error, code, request_id}chứa luôn contentexpecting→ client debug nhanh ở dev tool browser hoặc log monitoring.- Inner private
Phone(String)vs(pub String)— lợi ích + trade-off. Inner private (Phone(String)— tuple struct với field index 0 KHÔNG đánh dấupub): chỉ code trong moduledto/types.rstruy cập trực tiếpphone.0được; code ngoài module phải dùng accessoras_str()hoặcinto_inner(). Inner public (Phone(pub String)): mọi code dùngPhoneđều truy cậpphone.0trực tiếp như field public bình thường (B44 lock UserId/ProductId pattern). Lợi ích inner private: (i) Đảm bảo invariant normalized — cách duy nhất tạoPhonelà quaPhone::new(raw)Result-returning có normalize logic; dev không thể bypass tạoPhone("0912345678")raw chưa normalize qua tuple constructor. (ii) Compile-time enforce — refactor logic normalize chỉ sửa 1 chỗnormalize_vietnam_phone()+Phone::new(), mọi nơi dùngPhonetự động có format chuẩn không cần audit code. (iii) Security pattern cho Email B105 sau này — Email cần lowercase normalize + DNS check optional; nếu inner public dev có thể tạoEmail("[email protected]")chưa lowercase → leak vào DB unique constraint fail. Trade-off ergonomic: phải viết accessor methodas_str() -> &str+into_inner() -> Stringthay vì truy cậpphone.0direct. Code consume verbose hơn 1-2 char:format!("send to {}", phone.as_str())thayformat!("send to {}", phone.0); SQL bindsqlx::query!("... $1", phone.into_inner())thayphone.0. Acceptable trade-off — giá 1 method call cho security pattern + invariant guarantee. Phân biệt với UserId/ProductId B44 pub field: ID newtype KHÔNG có invariant phức tạp (chỉ wrap u64 cho type safety phân biệt UserId vs ProductId compile-time), không có rule normalize / validate runtime → inner public OK; dev tạoUserId(42)direct không break gì. Phone/Email/Money có rule fallible parse → inner private MANDATORY. Pattern lock Shop API B46: newtype có invariant runtime (Phone B46, Email B105, Money refactor B54) MANDATORY inner private + constructor Result-returning; newtype chỉ type safety zero-cost (UserId, ProductId, OrderId, CategoryId B44) thì inner public OK vớipub u64. Deserializer::deserialize_strvs Visitorvisit_str. Hai method tên giống nhau nhưng vai trò ngược chiều — đây là điểm dễ confuse cho dev mới với serde.Deserializer::deserialize_str(visitor): method trên traitDeserializer(do format implement: serde_json, serde_yaml, serde_toml, ...). Bạn gọi method này để báo cho format parser: "tôi expect 1 string value, optimize parse cho tôi". Format parser nhận hint, optimize: vd serde_json skip{/[prefix scan, đi thẳng vào string parsing không phải check toàn bộ JSON type. Method KHÔNG return value — returnResult<V::Value, Error>sau khi gọi back vàovisitorbạn cung cấp.
Visitorimpl<'de> Deserialize<'de> for Phone { fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> { de.deserialize_str(PhoneVisitor) // ← gợi ý format: "tôi expect string" } }visit_str(&str): method trên traitVisitor(do user implement: bạn). Format parser gọi method này để callback nhận value: "đây là string tôi parse được, xử lý nó". Method nhận&strđã parse, returnResult<Phone, E>sau khi convert.
2 chiều khác nhau:impl<'de> Visitor<'de> for PhoneVisitor { type Value = Phone; fn visit_str<E: de::Error>(self, value: &str) -> Result<Phone, E> { Phone::new(value).map_err(de::Error::custom) // ← callback xử lý value } }deserialize_*là hint bạn gửi cho format ("tôi expect kind này"),visit_*là callback format gọi ngược bạn ("đây là value tôi parse được"). Trade-off hint: cho phép format optimize parse path; format có thể IGNORE hint và gọivisit_*method khác nếu value thực tế khác kind hint (vd JSON gặp integer khi user hint string → fallback gọivisit_i64không phảivisit_str). Liên hệ pattern Rust traverse type không biết format: tương tự pattern double dispatch trong OOP (Visitor pattern Gang of Four 1994) — 1 object gặp data, KHÔNG biết concrete type của data, nên dispatch qua interface chung; data implement method, object cung cấp visitor handle method tương ứng kind data. Ưu điểm: extend kind mới (vd format MsgPack thêm bytes primitive) chỉ cần add method mới vào Visitor trait + format implement, KHÔNG break user impl cũ (default impl các method mới fallback error). Khác Visitor OOP GoF: serde Visitor type-driven (Rust trait generic) thay vì dynamic dispatch (OOP virtual function); zero-cost abstraction compile xuống direct call sau monomorphization. Pattern Rust traverse format-agnostic cũng dùng trong:std::fmt::Formatter(write_str / write_fmt cho Display impl chạy với mọi formatter),tokio::io::AsyncRead(poll_read trên reader generic chạy mọi backend file/socket),Iterator::next(lazy traverse element type-driven). Hiểu serde Visitor giúp đọc source ecosystem Rust rộng hơn.
Bài Tiếp Theo
Bài 47: Vec, HashMap Collection Trong JSON — collection serde behaviour: Vec<T>, HashMap<K,V>, BTreeMap<K,V>, HashSet<T>; key non-String trong Map; size limit chống DoS với serde_json::from_reader; áp ProductListResponse Shop API.
