Skip to content

Latest commit

 

History

History
268 lines (194 loc) · 7.35 KB

File metadata and controls

268 lines (194 loc) · 7.35 KB

17 — Serde Advanced (schema, defaults, strictness)

TOC · Prev · Next

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 แปลก ๆ หลุดเข้าระบบเงียบ ๆ


1) Reminder: เริ่มจาก Value แล้วค่อย tighten

ถ้า schema ยังไม่นิ่ง:

  • เริ่มจาก serde_json::Value
  • พอรู้ field ที่ใช้จริงแล้วค่อยทำ typed struct

pattern นี้อยู่ใน 09-config-and-serde-json.md


2) Default: #[serde(default)] และ default function

Python (เทียบแนวคิด):

  • ตั้ง default ด้วย .get(key, default) หรือกำหนดค่าใน constructor

Rust: ใช้ #[serde(default)] / #[serde(default = "...")] เพื่อ “รองรับของเก่าแบบตั้งใจ”

2.1 Default ของ field

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

2.2 Default ของทั้ง struct

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

3) Rename: rename และ rename_all

เวลา 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)

4) Strict mode: deny_unknown_fields

ปัญหาที่พบบ่อยในโลกจริง: 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 ในจุดที่ควบคุมได้

5) Optional + validation เบื้องต้น

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 คือชั้นที่ทำให้ระบบ “ไม่รับข้อมูลแปลก ๆ แบบเงียบ”

6) Enum modeling สำหรับหลายรูปแบบ (tagged enums)

ตัวอย่าง: 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)

7) แบบฝึกหัด (Exercises)

  1. ทำ struct ServerConfig ที่มี host: String, port: u16 และ port default 8080

  2. ใส่ #[serde(deny_unknown_fields)] แล้วลองเขียน JSON ที่มี field เกิน (คาดหวังว่า parse fail)

  3. ทำ enum LogLevel แบบ #[serde(rename_all = "lowercase")] ให้ parse ได้จาก string เช่น "info", "warn"

  4. ทำ struct AppConfig { auth: Auth } แล้ว deserialize JSON สองแบบ (none/apiKey)

  5. (ท้าทาย) ทำ validate() ให้เช็คว่า key ต้องยาว >= 10 เมื่อเป็น ApiKey