ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 동적인 요소를 가진 JSON을 깔끔하게 Unmarshal하기
    Go 2024. 5. 17. 19:17

    시작하며

    JSON API 클라이언트를 만들 때, 특정 요소가 동적인 값을 가지는 JSON을 Unmarshal해야 할 때가 있습니다.

     

    예를 들어, 아래와 같은 JSON입니다. 여기서 shape 요소의 값이 동적으로 변하는 상황을 가정합니다.

    {
        "id": "001",
        "type": "circle",
        "shape": {
            "radius": 5
        }
    }
    {
        "id": "002",
        "type": "rectangle",
        "shape": {
            "height": 5,
            "width": 2
        }
    }

     

    이런 JSON을 깔끔하게 Unmarshal하는 방법에 대해 간단히 소개해드리겠습니다.

    요점

    구조체의 동적인 요소는 interface로 정의합니다.

     

    동적인 요소는 interface로 정의하여, 임의의 구조체가 들어와도 동일한 방법으로 접근할 수 있게 합니다.

    type Drawer interface {
        Draw() string
    }
    
    type Figure struct {
        Id    string
        Type  string
        Shape Drawer
    }

     

    이번에는 동적인 요소로써 CircleRectangle이 적절히 interface를 만족하는 구현을 가지고 있습니다.

    type Circle struct {
        Radius int
    }
    
    type Rectangle struct {
        Height int
        Width  int
    }
    
    func (c *Circle) Draw() string {
        msg := fmt.Sprintf("draw with radius %d", c.Radius)
        return msg
    }
    
    func (r *Rectangle) Draw() string {
        msg := fmt.Sprintf("draw with height %d, width %d", r.Height, r.Width)
        return msg
    }

    Unmarshal할 때 에일리어스를 정의하고 동적인 요소를 json.RawMessage로 상속

    그대로 Figure를 Unmarshal하면 에러가 발생합니다.

     

    그래서 FigureUnmarshalJSON 내에서 에일리어스를 정의하고, 동적인 요소를 json.RawMessage로 상속합니다.

    type Alias Figure
    a := &struct {
        Shape json.RawMessage
        *Alias
    }{
        Alias: (*Alias)(f),
    }

     

    그리고 에일리어스를 사용해 Unmarshal합니다.

    if err := json.Unmarshal(data, &a); err != nil {
        return err
    }

     

    그러면 에일리어스가 동적인 요소를 json.RawMessage로 받아주므로, 이후 타입을 판별해 동적인 요소를 적절한 구조체로 Unmarshal한 뒤, 원래 구조체에 전달해줍니다.

    switch f.Type {
    case "circle":
        var c Circle
        if err := json.Unmarshal(a.Shape, &c); err != nil {
            return err
        }
        f.Shape = &c
    case "rectangle":
        var r Rectangle
        if err := json.Unmarshal(a.Shape, &r); err != nil {
            return err
        }
        f.Shape = &r
    default:
        return fmt.Errorf("unknown type: %q", f.Type)
    }

     

    UnmarshalJSON의 전체 모습은 다음과 같습니다.

    func (f *Figure) UnmarshalJSON(data []byte) error {
        type Alias Figure
        a := &struct {
            Shape json.RawMessage
            *Alias
        }{
            Alias: (*Alias)(f),
        }
        if err := json.Unmarshal(data, &a); err != nil {
            return err
        }
    
        switch f.Type {
        case "circle":
            var c Circle
            if err := json.Unmarshal(a.Shape, &c); err != nil {
                return err
            }
            f.Shape = &c
        case "rectangle":
            var r Rectangle
            if err := json.Unmarshal(a.Shape, &r); err != nil {
                return err
            }
            f.Shape = &r
        default:
            return fmt.Errorf("unknown type: %q", f.Type)
        }
        return nil
    }

    정리

    이렇게 작성하면, Figure를 Unmarshal할 때 적절한 구조체가 Shape 안에 들어가고, Draw()를 통해 접근할 수 있게 됩니다.

     

    마지막으로, 처음에 링크한 샘플을 전체적으로 붙여넣겠습니다.

    package main
    
    import (
        "encoding/json"
        "fmt"
        "log"
    )
    
    const circle = `
    {
        "id": "001",
        "type": "circle",
        "shape": {
            "radius": 5
        }
    }
    `
    
    const rectangle = `
    {
        "id": "002",
        "type": "rectangle",
        "shape": {
            "height": 5,
            "width": 2
        }
    }
    `
    
    type Drawer interface {
        Draw() string
    }
    
    type Figure struct {
        Id    string
        Type  string
        Shape Drawer
    }
    
    type Circle struct {
        Radius int
    }
    
    type Rectangle struct {
        Height int
        Width  int
    }
    
    func main() {
        var c, r Figure
        if err := json.Unmarshal([]byte(circle), &c); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("circle %s\n", c.Shape.Draw())
    
        if err := json.Unmarshal([]byte(rectangle), &r); err != nil {
            log.Fatal(err)
        }
        fmt.Printf("rectangle %s\n", r.Shape.Draw())
    }
    
    func (c *Circle) Draw() string {
        msg := fmt.Sprintf("draw with radius %d", c.Radius)
        return msg
    }
    
    func (r *Rectangle) Draw() string {
        msg := fmt.Sprintf("draw with height %d, width %d", r.Height, r.Width)
        return msg
    }
    
    func (f *Figure) UnmarshalJSON(data []byte) error {
        type Alias Figure
        a := &struct {
            Shape json.RawMessage
            *Alias
        }{
            Alias: (*Alias)(f),
        }
        if err := json.Unmarshal(data, &a); err != nil {
            return err
        }
    
        switch f.Type {
        case "circle":
            var c Circle
            if err := json.Unmarshal(a.Shape, &c); err != nil {
                return err
            }
            f.Shape = &c
        case "rectangle":
            var r Rectangle
            if err := json.Unmarshal(a.Shape, &r); err != nil {
                return err
            }
            f.Shape = &r
        default:
            return fmt.Errorf("unknown type: %q", f.Type)
        }
        return nil
    }

     

    실행 결과:

    circle draw with radius 5
    rectangle draw with height 5, width 2
Designed by Tistory.