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.Decoder의DisallowUnknownFields를 사용합니다. map[string]any를 디코딩하면 JSON 숫자는 기본적으로float64가 됩니다. 정밀도가 중요하면UseNumber를 검토합니다.- 태그 이름이 겹치는 임베디드 구조체는 필드 선택 규칙이 복잡하므로 API DTO를 명시적으로 정의하는 편이 안전합니다.