Go JSON 태그 활용

Go의 encoding/json은 구조체의 공개 필드를 JSON 객체로 변환합니다. 구조체 태그를 붙이면 키 이름, 생략 조건, 문자열 인코딩 여부를 필드별로 제어할 수 있습니다.

이름 지정

태그가 없으면 공개 필드 이름이 그대로 JSON 키가 됩니다. API 규격에 맞춰 소문자나 snake_case를 쓰려면 이름을 지정합니다.

type User struct {
	ID       int    `json:"id"`
	FullName string `json:"full_name"`
	Secret   string `json:"-"`
}

json:"-"은 인코딩과 디코딩 모두에서 해당 필드를 무시합니다. 소문자로 시작하는 비공개 필드는 태그를 붙여도 처리되지 않습니다.

omitempty

omitempty는 값이 비어 있을 때 필드를 생략합니다. false, 숫자 0, 빈 문자열, 길이 0인 배열·슬라이스·맵, nil 포인터와 인터페이스가 대상입니다.

type Profile struct {
	Name     string   `json:"name"`
	Age      int      `json:"age,omitempty"`
	Nickname *string  `json:"nickname,omitempty"`
	Tags     []string `json:"tags,omitempty"`
}

숫자 0이 “값이 없음”이 아니라 실제 값이라면 int 대신 *int를 사용해야 미입력(nil)과 0을 구분할 수 있습니다.

age := 0
p := Profile{Name: "LibRat", Age: age, Nickname: nil}

위 구조에서는 Age가 값 타입이므로 0일 때 빠집니다. 0도 보내야 한다면 omitempty를 제거하거나 포인터 필드로 모델링합니다.

string 옵션

string 옵션은 숫자나 불리언을 JSON 문자열 안에 넣습니다.

type Metric struct {
	Count int64 `json:"count,string"`
}
{"count":"42"}

상대 API가 문자열 형식의 숫자를 요구할 때만 사용하세요. 일반 숫자와 문자열 숫자를 혼용하면 클라이언트의 타입 처리가 복잡해집니다.

Marshal과 Unmarshal 확인

package main

import (
	"encoding/json"
	"fmt"
)

type User struct {
	Name  string `json:"name"`
	Age   int    `json:"age,omitempty"`
	Score int    `json:"score,string"`
}

func main() {
	in := User{Name: "LibRat", Score: 100}
	b, err := json.Marshal(in)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))

	var out User
	if err := json.Unmarshal(b, &out); err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", out)
}

결과는 {"name":"LibRat","score":"100"}입니다. 역직렬화 대상에는 반드시 포인터를 넘겨야 하며, string을 지정한 숫자 필드에 따옴표 없는 숫자가 오면 타입 오류가 발생합니다.

API에서 놓치기 쉬운 점

  • 알 수 없는 필드는 기본적으로 무시됩니다. 엄격히 검사하려면 json.DecoderDisallowUnknownFields를 사용합니다.
  • map[string]any를 디코딩하면 JSON 숫자는 기본적으로 float64가 됩니다. 정밀도가 중요하면 UseNumber를 검토합니다.
  • 태그 이름이 겹치는 임베디드 구조체는 필드 선택 규칙이 복잡하므로 API DTO를 명시적으로 정의하는 편이 안전합니다.

참고: Go encoding/json 공식 문서