Quick Syntax
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Nickname *string `json:"nickname,omitempty"`
Password string `json:"-"`
}JSON 필드(field)는 exported Go 필드만 기본 대상입니다. omitempty는 출력 생략 규칙이고, 입력에서 누락·null·영 값(zero value)을 구분해야 하면 포인터와 검증 규칙을 같이 정합니다.
필드와 tag
exported field만 기본으로 JSON 대상이 된다
type User struct {
ID string
name string
}ID는 대문자로 시작하므로 exported field이고, encoding/json이 기본으로 읽을 수 있습니다. name은 unexported field라 기본 JSON 변환 대상이 아닙니다.
struct tag로 JSON 이름을 정한다
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}Go 필드 이름은 ID, Name처럼 쓰고, JSON 표면은 id, name처럼 맞출 수 있습니다.
필드를 빼고 싶으면 -를 씁니다.
type User struct {
PasswordHash string `json:"-"`
}omitempty는 empty value일 때만 생략한다
type User struct {
Email string `json:"email,omitempty"`
Age int `json:"age,omitempty"`
}omitempty는 false, 0, nil pointer/interface, 길이 0인 array/slice/map/string을 출력에서 생략합니다. zero struct는 omitempty만으로 생략되지 않습니다. time.Time{}처럼 zero struct를 생략해야 할 때는 현재 encoding/json의 omitzero 옵션 또는 명시적인 포인터 표현을 프로젝트 지원 Go 버전과 함께 검토합니다.
type PatchUser struct {
Enabled *bool `json:"enabled,omitempty"`
Limit *int `json:"limit,omitempty"`
}*bool과 *int은 false나 0을 보내면서 필드 자체는 생략할 수 있게 합니다. 다만 기본 Unmarshal에서는 JSON 필드가 없을 때와 null일 때 모두 nil이 될 수 있으므로, 없음과 null까지 구분해야 하는 PATCH 계약은 사용자 정의 타입 또는 원본 JSON 검사로 별도 모델링합니다.
tag는 이름과 옵션을 함께 쓴다
type User struct {
ID string `json:"id"`
Name string `json:",omitempty"`
// PasswordHash string `json:"-"` // JSON 표면에서 항상 제외
}json:"id,omitempty"처럼 이름 뒤에 쉼표로 옵션을 붙입니다. json:",omitempty"는 Go field 이름을 그대로 쓰되 비어 있으면 생략합니다. tag를 붙여도 unexported field는 기본 변환 대상이 아니며, json:"-"는 민감한 내부 field가 응답에 섞이는 일을 막습니다.
Marshal과 Unmarshal
Marshal은 Go 값을 JSON 바이트로 바꾼다
data, err := json.Marshal(user)
if err != nil {
return err
}Marshal은 exported field를 따라 JSON 바이트를 만들고, 값이 json.Marshaler를 구현하면 그 MarshalJSON 결과를 사용합니다. []byte는 JSON array가 아니라 base64 문자열로 인코딩되고, NaN, +Inf, -Inf, 함수, channel, 순환 참조는 일반 JSON 값으로 만들 수 없어 error가 됩니다.
Unmarshal은 포인터를 받는다
var user User
if err := json.Unmarshal(data, &user); err != nil {
return err
}Unmarshal은 대상 값을 채워야 하므로 포인터가 필요합니다.
JSON object의 알려지지 않은 field는 기본적으로 무시되고, object key는 tag/field 이름과 정확히 맞는 것을 우선하되 대소문자 구분 없이도 맞출 수 있습니다. 외부 HTTP 입력에서 strict schema가 필요하면 HTTP JSON request와 response의 Decoder.DisallowUnknownFields 경계를 함께 둡니다.
재사용 대상은 먼저 비우거나 patch로 의도한다
var labels = map[string]string{"old": "kept"}
_ = json.Unmarshal([]byte(`{"new":"value"}`), &labels)
// labels: map[old:kept new:value]Unmarshal은 nil map이면 새 map을 만들지만 기존 map이면 없는 key를 지우지 않고 재사용합니다. 재사용 struct도 JSON에 없는 field를 자동으로 초기화하지 않습니다. 이전 값이 남으면 안 되는 전체 교체 입력은 새 변수에 decode하거나 대상 값을 zero value로 초기화하고, 일부 field만 바꾸는 PATCH만 재사용 의미를 갖게 합니다.
사용자 정의 JSON은 새 타입으로 재귀 호출을 피한다
func (u User) MarshalJSON() ([]byte, error) {
type userJSON User
return json.Marshal(userJSON(u))
}User 안에서 다시 json.Marshal(u)를 호출하면 같은 MarshalJSON이 재귀 호출됩니다. 새 정의 타입을 사용해 기본 필드 인코딩으로 돌아갑니다. UnmarshalJSON도 같은 방식으로 임시 타입에 decode한 뒤 검증과 대입을 마칩니다.
외부 입력은 구조 검증을 같이 본다
json.Unmarshal은 타입 변환은 해 주지만, 비즈니스 규칙을 검증하지는 않습니다.
if user.ID == "" {
return errors.New("missing id")
}필수 필드, 값 범위, enum 같은 규칙은 별도 검증으로 둬야 합니다.
주의할 점
Go JSON에서 가장 자주 생기는 오해는 omitempty가 검증이라는 착각입니다. 이는 출력 생략 규칙일 뿐이고, 입력 JSON의 필수 필드, 값 범위, 열거값, 알 수 없는 필드 허용 여부는 별도로 확인해야 합니다. 또한 unexported field는 struct tag를 붙여도 기본 JSON 변환 대상이 아닙니다.
참고 링크
2 sources