[CodeReview] Type Unmarshal กับการ Validate

TLDR: เมื่อสร้าง type ใหม่ขึ้นมา ไม่ควรรับค่านอกเหนือจาก domain ของ type นั้น
คำเตือน: บทความนี้เขียนจากความเห็นส่วนตัวล้วน ๆ

ลองดู​ code นี้

type Date struct {
time.Time
Valid bool
}

func (d Date) String() string {
return d.Time.Format("2006-01-02")
}

func (d Date) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}

func (d *Date) UnmarshalJSON(p []byte) error {
d.Time = time.Time{}
d.Valid = false

var s string
err := json.Unmarshal(p, &s)
if err != nil {
return err
}

d.Time, err = time.Parse("2006-01-02", s)
if err != nil {
return nil
}
d.Valid = true
return nil
}

จะเห็นว่า type Date ที่สร้างขึ้นมา ถ้า json ที่ส่งเข้ามาไม่สามารถ parse เป็น date ให้ จะไม่ error แต่ค่า Valid ที่อยู่ใน Date จะเป็น false

เวลาเรียกใช้ เราก็อาจจะต้องมา validate แบบนี้

type request struct {
BirthDate Date `json:"birthDate"`
}

func (req *request) Valid() error {
if !req.BirthDate.Valid {
return fmt.Errorf("invalid birthDate")
}
return nil
}

ตัวอย่าง unmarshal

var r request
err := json.Unmarshal([]byte(`{ "birthDate": "" }`), &r)
if err != nil {
fmt.Println("can not unmarshal;", err)
return
}
err = r.Valid()
if err != nil {
fmt.Println("validate failed;", err)
return
}

จะได้ error

validate failed; invalid birthDate

ตัวอย่างข้างบนนี้ จริง ๆ แล้วเราไม่ควรมา validate ว่า Date มัน valid เพราะค่าที่อยู่ใน Date มันควรจะเป็น Date

เหมือนกับถ้าเราเขียนว่า

type request struct {
Name string `json:"name"`
}

ค่าที่อยู่ใน Name มันก็ควรจะเป็น string ไม่คงไม่มา validate อีกทีว่าค่าใน Name เป็น string หรือไม่ใช่ string

เช่นเดียวกันกับ type Date ที่เราสร้างขึ้นมา เราก็ไม่ต้องมา validate ว่ามันเป็น Date หรือไม่ใช่ Date ถ้าไม่ใช่ Date มันควรจะไม่รับมาตั้งแต่แรก

ดังนั้นเราจึงสามารถเขียน UnmarshalJSON ได้ใหม่เป็น

type Date struct {
time.Time
}

func (d *Date) UnmarshalJSON(p []byte) error {
d.Time = time.Time{}

var s string
err := json.Unmarshal(p, &s)
if err != nil {
return err
}

d.Time, err = time.Parse("2006-01-02", s)
if err != nil {
return &json.UnmarshalTypeError{
Value: s,
Type: reflect.TypeOf(Date{}),
}
}
return nil
}

ถ้าค่าจาก json เข้ามาไม่สามารถ parse เป็น date ได้ ก็จะ error ทันทีตั้งแต่ตอน unmarshal ทำให้เราไม่จำเป็นต้องมา validate ว่า date เป็น date

แต่ใน function validate อย่าลืม validate วันที่ด้วยว่าอยู่ใน range ที่เราต้องการ


อีกตัวอย่าง

type Status int

const (
New Status = iota
Processing
Finished
)

func (s *Status) UnmarshalJSON(p []byte) error {
var x string
err := json.Unmarshal(p, &x)
if err != nil {
return err
}

switch x {
case "new":
*s = New
case "processing":
*s = Processing
case "finished":
*s = Finished
default:
return &json.UnmarshalTypeError{
Value: x,
Type: reflect.TypeOf(Status(0)),
}
}
return nil
}

จะเห็นว่า Status จะไม่สามารถรับค่าอื่นนอกจากค่าที่กำหนดได้เลย ทำให้เราไม่จำเป็นต้อง validate ว่า Status เป็น New, Processing, Finished ไหม


สรุป

เมื่อเราสร้าง type ขึ้นมาใหม่ เราไม่ควรรับค่าที่อยู่นอกเหนือ type นั้นเข้ามา
ค่าที่อยู่ใน type นั้น ควรจะเป็นค่าที่เราสนใจเท่านั้น