Keywords: serde, config, error handling, tooling
อ่านแบบคน Python:
- ถ้าอยากเอา “ภาพรวม” ก่อน: เป้าคือทำให้ “พิมพ์ key ผิด/ใส่ field แปลก” แล้วดังตั้งแต่ deserialize
- ถ้าอยาก “ลงมือทำ”: เริ่มจาก
default/renameแล้วค่อยพิจารณาdeny_unknown_fields - ถ้าติด: เปิด 12-learning-playbook.md
บทนี้ต่อยอดจาก 09-config-and-serde-json.md ให้คุณ parse JSON แบบ “เป็นระบบขึ้น”:
- ตั้งค่า default
- rename field
- ปฏิเสธ field แปลกปลอม (strict)
- model enum สำหรับหลายรูปแบบ
โฟกัสคือการทำ config/modeling ที่อ่านง่ายและปลอดภัย: ไม่ปล่อยให้ JSON แปลก ๆ หลุดเข้าระบบเงียบ ๆ
ถ้า schema ยังไม่นิ่ง:
- เริ่มจาก
serde_json::Value - พอรู้ field ที่ใช้จริงแล้วค่อยทำ typed struct
pattern นี้อยู่ใน 09-config-and-serde-json.md
Python (เทียบแนวคิด):
- ตั้ง default ด้วย
.get(key, default)หรือกำหนดค่าใน constructor
Rust: ใช้ #[serde(default)] / #[serde(default = "...")] เพื่อ “รองรับของเก่าแบบตั้งใจ”
use serde::Deserialize;
fn default_port() -> u16 {
8080
}
#[derive(Deserialize, Debug)]
struct ServerConfig {
host: String,
#[serde(default = "default_port")]
port: u16,
}Output (example):
(no output — types/functions definition only)
ถ้า JSON ไม่มี port → จะได้ 8080
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(default)]
struct Flags {
verbose: bool,
dry_run: bool,
}
impl Default for Flags {
fn default() -> Self {
Self { verbose: false, dry_run: false }
}
}Output (example):
(no output — types/impl definition only)
ข้อควรระวัง:
- default ทำให้ “missing field ไม่ fail”
- ใช้เมื่อคุณตั้งใจรองรับ config เวอร์ชันเก่า ไม่ใช่เพื่อกลบ typo
เวลา JSON เป็น camelCase แต่ Rust นิยม snake_case:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct ApiConfig {
base_url: String,
api_key: String,
}Output (example):
(no output — type definition only)
หรือ rename เฉพาะ field:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct ApiConfig2 {
#[serde(rename = "baseURL")]
base_url: String,
}Output (example):
(no output — type definition only)
ปัญหาที่พบบ่อยในโลกจริง: user พิมพ์ key ผิด แล้วระบบอ่านผ่านเงียบ ๆ
ถ้าต้องการ strict:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct StrictConfig {
host: String,
port: u16,
}Output (example):
(no output — type definition only)
เหมาะกับ:
- config ที่คุณคุม schema ได้
- ลด bug จาก typo เช่น
postแทนport
ไม่เหมาะกับ:
- config ที่หลายทีม/หลายเวอร์ชันเพิ่ม field ได้บ่อย (อาจทำให้ของเก่าอ่านไม่ผ่าน)
แนวบาลานซ์ (โยงบท 09):
- ใช้ struct แบบหลวมช่วง migrate (Option/default)
- เปิด strict ในจุดที่ควบคุมได้
Serde ทำให้ field “มี/ไม่มี” ได้ด้วย Option<T>:
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Cfg {
token: Option<String>,
}Output (example):
(no output — type definition only)
แล้วทำ validation เพิ่มเอง (semantics):
impl Cfg {
fn validate(&self) -> Result<(), String> {
if let Some(t) = &self.token {
if t.trim().is_empty() {
return Err("token must not be empty".to_string());
}
}
Ok(())
}
}Output (example):
(no output — impl definition only)
แนวคิดสำคัญ:
- schema ผ่าน ≠ ใช้ได้จริง
- validate คือชั้นที่ทำให้ระบบ “ไม่รับข้อมูลแปลก ๆ แบบเงียบ”
ตัวอย่าง: field auth อาจเป็น {"type":"none"} หรือ {"type":"apiKey","key":"..."}
use serde::Deserialize;
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
enum Auth {
#[serde(rename = "none")]
None,
#[serde(rename = "apiKey")]
ApiKey { key: String },
}Output (example):
(no output — type definition only)
ข้อดี: match ได้ชัดและ “ลืมเคส” ยาก
fn use_auth(a: &Auth) {
match a {
Auth::None => {}
Auth::ApiKey { key } => {
println!("key len = {}", key.len());
}
}
}Output (example):
(no output — function definition only)
ทิป:
- ถ้า key เป็นความลับ อย่าพิมพ์ค่าจริง ให้พิมพ์แค่ความยาวหรือ
(redacted)
-
ทำ struct
ServerConfigที่มีhost: String,port: u16และportdefault 8080 -
ใส่
#[serde(deny_unknown_fields)]แล้วลองเขียน JSON ที่มี field เกิน (คาดหวังว่า parse fail) -
ทำ
enum LogLevelแบบ#[serde(rename_all = "lowercase")]ให้ parse ได้จาก string เช่น"info","warn" -
ทำ
struct AppConfig { auth: Auth }แล้ว deserialize JSON สองแบบ (none/apiKey) -
(ท้าทาย) ทำ
validate()ให้เช็คว่า key ต้องยาว >= 10 เมื่อเป็นApiKey