DEV Community

loading...

Unmarshal JSON ที่มีวันที่ format แปลกๆใน Go

Atthavit Wannasakwong
・3 min read

พอดีวันนี้เขียน Go แล้วเจอปัญหานึงคือ จะต้องไปดึงข้อมูลจาก API นึง แต่ API นั้นดัน return ข้อมูลวันที่มาไม่ตาม standard ที่ json.Unmarshal สามารถแปลงได้



ปัญหา

{
    "date": "2021/06/09 18:20"
}
Enter fullscreen mode Exit fullscreen mode

จากตัวอย่าง JSON จะเห็นว่า format มันแปลกๆ ไม่ใช่ RFC3339 ที่ time.Time ใช้ในการ implement json.Unmarshaler

ถ้าเราลอง Unmarshal ใส่ struct ที่มี time.Time ปกติ

type Response struct {
    Date time.Time `json:"date"`
}
data := []byte(`{
    "date": "2021/06/09 18:20"
}`)

var resp Response
err := json.Unmarshal(data, &resp)
if err != nil {
    log.Fatalf("error: %v", err)
}
Enter fullscreen mode Exit fullscreen mode

มันก็จะ errror

error: parsing time ""2021/06/09 18:20"" as ""2006-01-02T15:04:05Z07:00"": cannot parse "/06/09 18:20"" as "-"
Enter fullscreen mode Exit fullscreen mode

วิธีแรก เพิ่ม field ไปเลยแบบง่ายๆ

วิธีแรกที่นึกออกแบบเร็วๆคือ unmarshal เป็น string มาก่อน แล้วค่อยแปลง string นั้นเป็น time.Time อีกที

เริ่มจากสร้าง struct แบบให้ date ถูก unmarshal เข้า DateStr

type Response struct {
    Date    time.Time `json:"-"`
    DateStr string    `json:"date"`
}
Enter fullscreen mode Exit fullscreen mode

แล้วมี method แปลง string นั้นเป็น time.Time อีกที

func (r *Response) Parse() error {
    t, err := time.Parse(DateFormat, r.DateStr)
    if err != nil {
        return err
    }
    r.Date = t
    return nil
}
Enter fullscreen mode Exit fullscreen mode

ตอนใช้ก็คือต้องไม่ลืมเรียก Parse() ต่อจาก Unmarshal()

data := []byte(`{
    "date": "2021/06/09 18:20"
}`)

var resp Response
err := json.Unmarshal(data, &resp)
if err != nil {
    log.Fatalf("error: %v", err)
}
if err := resp.Parse(); err != nil {
    log.Fatalf("cannot parse: %v", err)
}
fmt.Println(resp.Date)
j, _ := json.Marshal(resp)
fmt.Println(string(j))
Enter fullscreen mode Exit fullscreen mode

จะได้ output ออกมาเป็น

2021-06-09 18:20:00 +0000 UTC
{"date":"2021/06/09 18:20"}
Enter fullscreen mode Exit fullscreen mode

ปัญหาของวิธีนี้คือ

  1. ต้องไม่ลืมเรียก method ที่ใช้ในการ parse time
  2. ถ้าเอา struct ที่ได้ ไป json.Marshal ใหม่ ก็จะได้เป็น format ผิดๆเหมือนเดิม

วิธีที่สอง implement json.Unmarshaler interface

จากที่อ่านผ่านๆมาจะเจอคนแนะนำวิธีนี้เยอะ

That's a case when you need to implement custom marshal and unmarshal functions.

UnmarshalJSON(b []byte) error { ... }
MarshalJSON() ([]byte, error) { ... }

By following the example in the Golang documentation of json package you get something like:

// First create a type alias
type JsonBirthDate time.Time

//

วิธีนี้คือสร้าง type alias ของ time.Time ขึ้นมาเลย แล้ว implement json.Unmarshaler ให้มัน ส่วนตัวผมรู้สึกว่ามันก็เขียนง่ายดี แต่เวลาเอาไปใช้ มันจะใช้ยาก

เช่น มี struct แบบนี้

type Response struct {
    Date DateTime `json:"date"`
}
type DateTime time.Time
func (dt *DateTime) UnmarshalJSON(bs []byte) error {
    ...
}
func (dt *DateTime) MarshalJSON() ([]byte, error) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

ถ้าเราจะเรียก function นึง โดยที่ function นั้นต้องการ time.Time เราก็ต้องคอยมา convert type ให้ตลอด ทำให้มันดูไม่ค่อยดีเท่าไหร่ สู้ทำดีๆตั้งแต่แรกแล้วใช้ง่ายๆทีหลังดีกว่า

func PrintTomorrow(t time.Time) {
    fmt.Println(t.Add(24 * time.Hour))
}
func main() {
    resp := Response{}
    // PrintTomorrow(resp.Date)  // error: cannot use resp.Date (type DateTime) as type time.Time
    PrintTomorrow(time.Time(resp.Date))
}
Enter fullscreen mode Exit fullscreen mode

วิธีที่สาม สร้าง type ใหม่เพื่อ unmarshal โดยเฉพาะ

หลังจากลองเขียนวิธีแรกกับวิธีสองแล้วรู้สึกมันแปลกๆ ไม่ค่อยชอบเท่าไหร่ ก็เลยไปลองเพิ่ม เจออีกวิธีที่น่าสนใจจาก stackoverflow วิธีนี้อาจจะดูซับซ้อนหน่อยแต่พอทำแล้วใช้ง่ายขึ้นเยอะ ไม่มีปัญหาแบบวิธีแรก

An easy and common way to avoid this / protect from it is to create a new type with the type keyword, and use a type conversion to pass a value of this type (the value may be your original value, type conversion is possible because the new type has…

เริ่มจากสร้าง struct สำหรับ Unmarshal JSON ใส่ struct tag ให้ครบเหมือนเดิม

type response struct {
    Date    time.Time `json:"-"`
    DateStr string    `json:"date"`
}
Enter fullscreen mode Exit fullscreen mode

แล้วสร้าง method UnmarshalJSON เพื่อ implement json.Unmarshaler เวลาเรียก json.Unmarshal มันก็จะมาเรียก method นี้แทน

func (r *response) UnmarshalJSON(bs []byte) error {
    type responsePtr *response
    err := json.Unmarshal(bs, responsePtr(r))
    if err != nil {
        return err
    }
    t, err := time.Parse(DateFormat, r.DateStr)
    if err != nil {
        return err
    }
    r.Date = t
    return nil
}
Enter fullscreen mode Exit fullscreen mode

code ข้างบนมีจุดที่น่าสนใจอยู่คือตรงที่มีสร้าง type alias ของ *response

type responsePtr *response
err := json.Unmarshal(bs, responsePtr(r))
Enter fullscreen mode Exit fullscreen mode

ที่ต้องทำแบบนี้ก็เพราะ ถ้าเราเรียก json.Unmarshal เข้า struct เดิม ใน response.UnmarshalJSON มันก็จะมาเรียก response.UnmarshalJSON วนไปเรื่อยๆจน stack overflow

แต่ถ้าเราสร้าง type alias แล้ว method UnmarshalJSON มันจะไม่ตามไปด้วย เวลาเราเรียก json.Unmarshal มันเช็ค type responsePtr ก็จะเห็นว่ามันไม่ได้ implement json.Unmarshaler interface มันก็จะ unmarshal แบบปกติเลย

ทีนี้พอใช้ type alias ในการ unmarshal ได้แล้ว เราก็ค่อยเอา string ที่ได้มา parse แล้วใส่ค่าให้อีก field อันนี้ก็จะแก้ปัญหาที่ 1 ของวิธีแรก ของวิธีเมื่อกี้ได้แล้ว

สำหรับปัญหาที่ 2 ของวิธีแรก ก็สามารถแก้ได้ด้วยการสร้าง struct ขึ้นมาอีกอัน ที่มี struct tag json:"date" ใน field Date แทน และเนื่องจาก type response กับ Response มี fields เหมือนกันเลย เราจึงสามารถ convert เป็นอีก type ได้เลย

type Response struct {
    Date    time.Time `json:"date"`
    DateStr string    `json:"-"`
}
Enter fullscreen mode Exit fullscreen mode

วิธีใช้ก็คือ json.Unmarshal เข้า type response แล้ว convert ให้เป็น type Response อีกที

data := []byte(`{
    "date": "2021/06/09 18:20"
}`)
var resp response
err := json.Unmarshal(data, &resp)
if err != nil {
    log.Fatalf("error: %v", err)
}
result := Response(resp)
fmt.Println(result.Date)
j, _ := json.Marshal(result)
fmt.Println(string(j))
Enter fullscreen mode Exit fullscreen mode

output json ที่ออกมาก็จะได้เป็น format ที่ถูก เผื่อเราต้องส่งไปให้ระบบอื่น เค้าก็จะไม่ต้องมาแก้ปัญหาเดียวกับเราอีก

2021-06-09 18:20:00 +0000 UTC
{"date":"2021-06-09T18:20:00Z"}
Enter fullscreen mode Exit fullscreen mode

โค้ดทั้งหมดอยู่ที่นี่นะครับ
https://github.com/atthavit/myblog/tree/master/unmarshal-json

Discussion (0)