시작하며
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
}
이번에는 동적인 요소로써 Circle
과 Rectangle
이 적절히 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하면 에러가 발생합니다.
그래서 Figure
의 UnmarshalJSON
내에서 에일리어스를 정의하고, 동적인 요소를 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
'Go' 카테고리의 다른 글
Go 언어로 문자열 결합 최적화하기: strings.Builder 완벽 가이드 및 벤치마크 (0) | 2024.05.19 |
---|---|
Go 언어 슬라이스 완벽 이해 - 구현과 활용 (0) | 2024.05.19 |
Go에서의 소수점 연산과 오차 처리 (0) | 2024.05.17 |
json.Unmarshal 사용시 타임(time) 형식을 유연하게 변경하는 방법 (0) | 2024.05.04 |
Go 언어 인터페이스 구현 패턴 (0) | 2024.04.14 |