Marshal과 Unmarshal
json.Marshal은 Go 값을 JSON 바이트로 바꿉니다. json.Unmarshal은 JSON 바이트로 Go 값을 채웁니다. 구조체 태그가 JSON 필드 이름을 정합니다.
첫 번째 출력에 Email이 없는 이유는 omitempty와 빈 문자열 때문입니다. Unmarshal에는 포인터(&back)가 필요합니다. 값 자체를 넘기면 InvalidUnmarshalError가 반환됩니다.
구조체 태그
태그 문법은 json:"name,option,option"입니다.
| 태그 | 효과 |
|---|---|
json:"user_id" | user_id를 키로 사용 |
json:"email,omitempty" | false, 0, "", nil, 빈 슬라이스나 맵이면 생략 |
json:",omitzero" (Go 1.24) | 값이 제로 값이거나 IsZero()가 true를 반환하면 생략 |
json:"-" | 이 필드를 절대 인코딩하거나 디코딩하지 않음 |
json:"-," | 글자 그대로 -를 키로 사용 |
json:"count,string" | 숫자나 bool을 JSON 문자열("42")로 인코딩 |
| 태그 없음 | 키는 Go 필드 이름 UserID |
비밀번호는 구조체 밖으로 나가지 않고, 잔액은 따옴표로 감싸지며, deleted_at은 사라지는 반면 created_at은 제로 시간 0001-01-01T00:00:00Z를 출력합니다. omitzero가 추가된 이유가 이 차이입니다. omitempty는 구조체에 동작한 적이 없고, time.Time에서 오래도록 사람들을 놀라게 해 왔습니다. Go 1.23 이하에서는 omitempty와 함께 *time.Time을 쓰면 같은 효과를 얻을 수 있습니다.
MarshalIndent(v, prefix, indent)는 읽기 좋은 출력을 만듭니다. 설정 파일과 디버깅에 쓰세요. API는 보통 압축된 JSON을 보냅니다.
공개 필드만
encoding/json은 리플렉션을 쓰며 공개 필드만 볼 수 있습니다. Go에서 가장 흔한 JSON 버그입니다:
type point struct {
x, y int // lowercase: json.Marshal(point{1, 2}) gives {}
}
오류도 경고도 없이, 나갈 때는 {}가 되고 들어올 때는 필드가 제로 값으로 남습니다. 필드 이름을 대문자로 시작하게 하고, 소문자 키는 태그로 붙이세요.
디코딩이 필드를 매칭하는 방식
구조체로 언마샬링할 때:
- 키는 태그 이름이나 필드 이름과 대소문자를 구분하지 않고 매칭됩니다.
{"NAME": "x"}는json:"name"태그가 붙은 필드를 채웁니다. - 일치하는 필드가 없는 키는 조용히 무시됩니다.
- 일치하는 키가 없는 필드는 현재 값을 유지합니다. Unmarshal은 필드를 초기화하지 않으므로, 이미 데이터가 있는 구조체로 디코딩하면 그 위에 합쳐집니다.
- 타입 불일치(구조체에는
int인데 문자열이 옴)는*json.UnmarshalTypeError를 반환하지만, 다른 필드는 여전히 채워집니다.
엄격한 API나 오타가 있을 수 있는 설정 파일처럼 예상하지 못한 키를 거부하려면 DisallowUnknownFields를 설정한 Decoder를 쓰세요:
구조를 모를 때: map[string]any
모양을 미리 모를 때는 map[string]any나 any로 디코딩하세요. 대응 관계는 고정되어 있습니다:
| JSON | Go |
|---|---|
| 객체 | map[string]any |
| 배열 | []any |
| 문자열 | string |
| 숫자 | float64 |
| true / false | bool |
| null | nil |
출력에서 함정 두 가지가 보입니다. 모든 숫자가 float64이므로 m["stock"].(int)는 실패합니다. 그리고 2^53보다 큰 정수는 float64에서 정밀도를 잃습니다. ID 9007199254740993은 ...992로 돌아옵니다. UseNumber로 둘 다 피할 수 있습니다.
구조의 일부를 안다면 그 부분은 구조체로 디코딩하고 나머지는 json.RawMessage로 받으세요. 필드의 원시 바이트를 담아 두었다가 타입을 알게 된 뒤에 디코딩할 수 있습니다.
슬라이스, 맵, 포인터, nil
| Go 값 | JSON |
|---|---|
nil 슬라이스 (var s []int) | null |
빈 슬라이스 ([]int{}) | [] |
nil 맵 | null |
nil 포인터 | null |
[]byte | base64 문자열 |
map[string]T | 객체, 키는 정렬됨 |
map[int]T | 정수 키를 문자열로 바꾼 객체 |
배열을 기대하는 API 클라이언트에게는 nil과 빈 슬라이스의 차이가 중요합니다. 필드가 반드시 []여야 한다면 []T{}나 make([]T, 0)로 초기화하세요.
입력에서 "없음"과 "0"을 구분해야 한다면 포인터 필드(*int, *bool)를 쓰세요. 언마샬링한 뒤 nil 포인터는 키가 없었거나 null이었다는 뜻이고, 0을 가리키는 포인터는 클라이언트가 0을 보냈다는 뜻입니다.
스트림: Encoder와 Decoder
json.Marshal과 Unmarshal은 바이트 슬라이스 전체를 다룹니다. io.Reader나 io.Writer(HTTP 본문, 파일, 표준 입력)에는 json.NewDecoder와 json.NewEncoder를 쓰세요. Decoder는 연속된 JSON 값을 하나씩 읽을 수도 있습니다:
Encoder.Encode는 각 값 뒤에 줄바꿈을 씁니다. 기본적으로 Marshal과 Encoder는 모두 <, >, &를 \u003c, \u003e, \u0026으로 이스케이프하므로 JSON을 HTML에 넣어도 안전합니다. SetEscapeHTML(false)로 이를 끌 수 있습니다. HTTP 핸들러에서는 json.NewDecoder(r.Body).Decode(&v)와 json.NewEncoder(w).Encode(v)가 흔히 쓰는 짝입니다.
직접 인코딩 정하기
타입은 json.Marshaler와 json.Unmarshaler를 구현해서 자신의 JSON을 직접 정할 수 있습니다. 숫자가 아니라 이름으로 나타나야 하는 iota 열거형이 흔한 경우입니다:
MarshalJSON은 값과 포인터 모두에 동작하도록 값 리시버를 쓰고, UnmarshalJSON은 값을 수정하므로 포인터 리시버가 필요합니다. 문자열 형태만 필요한 타입은 대신 encoding.TextMarshaler(MarshalText)를 구현할 수 있으며, 그러면 맵 키로도 쓸 수 있습니다.
흔한 실수
- 소문자 필드 이름. 조용히 건너뜁니다.
Unmarshal에 값을 넘김. 포인터가 필요합니다.- 오류를 무시함. 잘못된 JSON과 타입 불일치는 오류로만 보고됩니다.
map[string]any의 숫자를int로 가정함.float64입니다.omitempty가 빈 구조체나 제로 값time.Time을 뺄 거라고 기대함. Go 1.24라면omitzero를, 아니면 포인터를 쓰세요.- 맵의 필드 순서에 의존함. 맵 키는 출력할 때 정렬되고, 구조체 필드는 선언 순서를 유지합니다.
자주 묻는 질문
Go에서 구조체를 JSON으로 바꾸려면 어떻게 하나요?
[]byte와 오류를 반환하는 json.Marshal(v)를 호출합니다. 공개 필드(대문자로 시작하는 이름)만 포함됩니다. 필드에 json:"name" 같은 구조체 태그를 달면 JSON 키를 정할 수 있습니다. 들여쓰기된 출력이 필요하면 json.MarshalIndent(v, "", " ")를 쓰세요.
구조체 필드가 JSON 출력에서 빠지는 이유는 무엇인가요?
encoding/json은 공개 필드만 봅니다. name(소문자)이라는 필드는 마샬링할 때도 언마샬링할 때도 보이지 않습니다. 필드 이름을 대문자로 시작하게 하고 json:"name" 같은 태그로 JSON 키를 정하세요.
Go JSON에서 omitempty는 무엇을 하나요?
omitempty는 필드가 빈 값(false, 0, "", nil 포인터나 인터페이스, 빈 슬라이스나 맵)을 담고 있으면 출력에서 뺍니다. 구조체나 time.Time은 비어 있다고 취급하지 않습니다. Go 1.24에는 구조체와 time.Time을 포함해 타입의 제로 값인 값(또는 IsZero() 메서드가 true를 반환하는 값)을 빼는 omitzero가 추가되었습니다.
Go에서 구조를 모르는 JSON은 어떻게 파싱하나요?
map[string]any(또는 any)로 언마샬링합니다. 객체는 map[string]any, 배열은 []any, 문자열은 string, 불리언은 bool, 모든 숫자는 float64가 됩니다. 값을 읽을 때는 타입 단언을 쓰고, 큰 정수를 정확히 유지해야 한다면 Decoder.UseNumber를 쓰세요.