본문 바로가기
프로그래밍/Go

Go JSON과 XML 처리하기

by JLearn 2026. 7. 28.
반응형

Go 표준 라이브러리의 encoding/jsonencoding/xml 패키지는 Go 값과 JSON·XML 문서 사이의 변환을 지원합니다.

두 패키지는 구조체를 처리할 때 reflection을 사용하며, 기본적으로 외부에서 접근할 수 있는 exported field만 변환합니다. 필드 이름이나 생략 조건은 struct tag로 지정할 수 있습니다.

JSON과 XML은 비슷한 방식으로 사용할 수 있지만 표현 구조와 struct tag 규칙은 서로 다릅니다. JSON은 객체의 key와 구조체 필드를 대응시키고, XML은 element, attribute, 문자 데이터와 구조체 필드를 대응시킵니다.

외부에서 받은 JSON이나 XML을 바로 디코딩할 때는 문법 검사만으로 충분하지 않습니다. 입력 크기를 제한하고, 필요한 필드와 허용할 필드 정책을 애플리케이션에서 정해야 합니다.


핵심 개념

구분 JSON XML
패키지 encoding/json encoding/xml
Go 값을 문서로 변환 json.Marshal xml.Marshal
문서를 Go 값으로 변환 json.Unmarshal xml.Unmarshal
stream 읽기 json.Decoder xml.Decoder
stream 쓰기 json.Encoder xml.Encoder
field tag json:"name,omitempty" xml:"name,omitempty"
기본 구조 object, array, string, number 등 element, attribute, character data 등
알 수 없는 필드 기본적으로 무시, DisallowUnknownFields 사용 가능 구조체와 대응되지 않는 element·attribute는 일반적으로 무시

Marshal은 Go 값을 []byte로 변환하고, Unmarshal은 완전한 []byte 데이터를 Go 값에 저장합니다.

EncoderDecoderio.Writerio.Reader를 사용하므로 파일, HTTP body, 표준 입출력처럼 stream을 처리할 때 적합합니다.


디렉터리 구조는 다음과 같이 구성합니다.

sources/
└── json-xml/
    └── main.go

sources 디렉터리에서 실행합니다.

cd sources
go run ./json-xml

JSON 기본 변환

다음 구조체를 JSON으로 변환해 보겠습니다.

type Product struct {
    ID       int      `json:"id"`
    Name     string   `json:"name"`
    Price    int      `json:"price"`
    Tags     []string `json:"tags,omitempty"`
    Internal string   `json:"-"`
    note     string
}

각 필드의 의미는 다음과 같습니다.

선언 의미
json:"id" JSON key를 id로 지정
json:"tags,omitempty" 값이 비어 있으면 tags 생략
json:"-" JSON 변환 대상에서 항상 제외
note string 소문자로 시작하는 unexported field이므로 변환되지 않음

실행 예제:

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Product struct {
    ID       int      `json:"id"`
    Name     string   `json:"name"`
    Price    int      `json:"price"`
    Tags     []string `json:"tags,omitempty"`
    Internal string   `json:"-"`
    note     string
}

func main() {
    product := Product{
        ID:       1001,
        Name:     "키보드",
        Price:    89000,
        Tags:     []string{"입력장치", "USB"},
        Internal: "관리용 메모",
        note:     "외부에 공개하지 않음",
    }

    data, err := json.Marshal(product)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(data))
}

출력:

{"id":1001,"name":"키보드","price":89000,"tags":["입력장치","USB"]}

Internaljson:"-"가 지정되어 제외되고, note는 exported field가 아니므로 제외됩니다.

사람이 읽기 편한 형태가 필요하면 json.MarshalIndent를 사용할 수 있습니다.

data, err := json.MarshalIndent(product, "", "  ")

출력:

{
  "id": 1001,
  "name": "키보드",
  "price": 89000,
  "tags": [
    "입력장치",
    "USB"
  ]
}

JSON을 구조체로 변환하기

JSON의 구조를 알고 있다면 map[string]any보다 명확한 구조체로 디코딩하는 것이 좋습니다.

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Product struct {
    ID    int      `json:"id"`
    Name  string   `json:"name"`
    Price int      `json:"price"`
    Tags  []string `json:"tags"`
}

func main() {
    input := []byte(`{
        "id": 1001,
        "name": "키보드",
        "price": 89000,
        "tags": ["입력장치", "USB"]
    }`)

    var product Product

    if err := json.Unmarshal(input, &product); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("ID: %d\n", product.ID)
    fmt.Printf("이름: %s\n", product.Name)
    fmt.Printf("가격: %d\n", product.Price)
    fmt.Printf("태그: %v\n", product.Tags)
}

출력:

ID: 1001
이름: 키보드
가격: 89000
태그: [입력장치 USB]

Unmarshal의 두 번째 인수에는 결과를 저장할 수 있도록 포인터를 전달해야 합니다.

json.Unmarshal(input, &product)

&product가 아니라 product를 전달하면 Unmarshal이 값을 변경할 수 없으므로 오류가 발생합니다.


struct tag와 omitempty

omitempty는 필드 값이 비어 있다고 판단될 때 해당 필드를 출력에서 생략합니다.

JSON v1 패키지에서 일반적으로 비어 있다고 판단되는 값은 다음과 같습니다.

  • false
  • 숫자 0
  • 빈 문자열 ""
  • 길이가 0인 array, slice, map
  • nil pointer 또는 interface
package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type User struct {
    ID       int      `json:"id"`
    Name     string   `json:"name"`
    Nickname string   `json:"nickname,omitempty"`
    Roles    []string `json:"roles,omitempty"`
    Active   bool     `json:"active,omitempty"`
}

func main() {
    user := User{
        ID:   1,
        Name: "홍길동",
    }

    data, err := json.Marshal(user)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(data))
}

출력:

{"id":1,"name":"홍길동"}

Nickname은 빈 문자열이고, Rolesnil, Activefalse이므로 생략됩니다.

값이 없다는 상태와 0을 구분하기

omitempty를 사용하면서 0이나 false를 실제 값으로 보내야 한다면 pointer를 사용할 수 있습니다.

type UpdateRequest struct {
    Stock  *int  `json:"stock,omitempty"`
    Active *bool `json:"active,omitempty"`
}
zero := 0
inactive := false

request := UpdateRequest{
    Stock:  &zero,
    Active: &inactive,
}

pointer 자체가 nil이 아니므로 다음과 같이 출력됩니다.

{"stock":0,"active":false}

이 방식은 부분 수정 API에서 다음 상태를 구분할 때 유용합니다.

  • 필드를 보내지 않음: pointer가 nil
  • 재고를 0으로 변경: Stock&zero
  • 활성 상태를 false로 변경: Active&inactive

알 수 없는 JSON 필드 처리

json.Unmarshal과 기본 json.Decoder는 구조체에 없는 JSON 필드를 기본적으로 무시합니다.

예를 들어 구조체에 name만 있어도 입력의 admin은 오류가 되지 않습니다.

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type UserRequest struct {
    Name string `json:"name"`
}

func main() {
    input := []byte(`{"name":"홍길동","admin":true}`)

    var request UserRequest
    if err := json.Unmarshal(input, &request); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", request)
}

출력:

{Name:홍길동}

API 요청처럼 허용되지 않은 필드를 오류로 처리하려면 Decoder.DisallowUnknownFields를 사용합니다.

package main

import (
    "encoding/json"
    "fmt"
    "strings"
)

type UserRequest struct {
    Name string `json:"name"`
}

func main() {
    input := `{"name":"홍길동","admin":true}`

    decoder := json.NewDecoder(strings.NewReader(input))
    decoder.DisallowUnknownFields()

    var request UserRequest
    if err := decoder.Decode(&request); err != nil {
        fmt.Println("디코딩 실패:", err)
        return
    }

    fmt.Printf("%+v\n", request)
}

출력:

디코딩 실패: json: unknown field "admin"

DisallowUnknownFields는 오타가 있는 요청이나 서버가 지원하지 않는 필드를 조기에 발견하는 데 도움이 됩니다.

DisallowUnknownFields는 JSON 객체를 구조체로 디코딩할 때 의미가 있습니다. map[string]any는 임의의 key를 받기 위한 타입이므로 알 수 없는 필드라는 개념을 적용할 수 없습니다.


JSON 숫자와 any

JSON 숫자를 any 또는 map[string]any로 디코딩하면 기본적으로 float64가 됩니다.

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

func main() {
    input := []byte(`{"id":1001,"price":89000}`)

    var value map[string]any
    if err := json.Unmarshal(input, &value); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("id 값: %v, 타입: %T\n", value["id"], value["id"])
    fmt.Printf("price 값: %v, 타입: %T\n", value["price"], value["price"])
}

출력:

id 값: 1001, 타입: float64
price 값: 89000, 타입: float64

구조를 알고 있다면 숫자 타입을 명확히 선언한 구조체로 디코딩하는 것이 가장 단순합니다.

type Product struct {
    ID    int64 `json:"id"`
    Price int64 `json:"price"`
}

UseNumber로 숫자 표현 보존하기

입력 구조를 미리 알 수 없어 any를 사용해야 한다면 Decoder.UseNumber를 사용할 수 있습니다.

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "strings"
)

func main() {
    input := `{"id":9007199254740993,"price":89000}`

    decoder := json.NewDecoder(strings.NewReader(input))
    decoder.UseNumber()

    var value map[string]any
    if err := decoder.Decode(&value); err != nil {
        log.Fatal(err)
    }

    idNumber, ok := value["id"].(json.Number)
    if !ok {
        log.Fatal("id가 json.Number가 아닙니다")
    }

    id, err := idNumber.Int64()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("원본 표현: %s\n", idNumber.String())
    fmt.Printf("int64 값: %d\n", id)
    fmt.Printf("저장 타입: %T\n", value["id"])
}

출력:

원본 표현: 9007199254740993
int64 값: 9007199254740993
저장 타입: json.Number

json.Number는 숫자를 문자열 형태로 보관하고 Int64, Float64 메서드로 필요한 타입으로 변환합니다. 변환할 때 범위와 형식 오류를 확인할 수 있습니다.


Marshal·UnmarshalEncoder·Decoder

두 방식의 차이는 입력과 출력 형태에 있습니다.

방식 입력 또는 출력 적합한 상황
json.Marshal Go 값 → []byte 메모리에서 완성된 JSON 데이터가 필요할 때
json.Unmarshal []byte → Go 값 완성된 JSON byte slice를 한 번에 읽을 때
json.Encoder Go 값 → io.Writer 파일, HTTP 응답, 표준 출력에 직접 쓸 때
json.Decoder io.Reader → Go 값 HTTP body, 파일, 연결 stream에서 읽을 때

Encoder 예제

package main

import (
    "encoding/json"
    "log"
    "os"
)

type Event struct {
    Type string `json:"type"`
    ID   int    `json:"id"`
}

func main() {
    encoder := json.NewEncoder(os.Stdout)
    encoder.SetIndent("", "  ")

    event := Event{
        Type: "created",
        ID:   1001,
    }

    if err := encoder.Encode(event); err != nil {
        log.Fatal(err)
    }
}

Encoder.Encode는 JSON 값 뒤에 줄바꿈을 추가합니다. 여러 JSON 값을 연속해서 stream으로 쓸 때 편리합니다.

여러 JSON 값을 stream으로 읽기

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "strings"
)

type Event struct {
    Type string `json:"type"`
    ID   int    `json:"id"`
}

func main() {
    input := `
        {"type":"created","id":1001}
        {"type":"updated","id":1002}
        {"type":"deleted","id":1003}
    `

    decoder := json.NewDecoder(strings.NewReader(input))

    for {
        var event Event

        err := decoder.Decode(&event)
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }

        fmt.Printf("%s: %d\n", event.Type, event.ID)
    }
}

출력:

created: 1001
updated: 1002
deleted: 1003

하나의 JSON 값만 허용하기

HTTP 요청 body에서 Decode를 한 번만 호출하면 첫 번째 JSON 값 뒤에 추가 JSON 값이 있어도 놓칠 수 있습니다.

예를 들어 다음 입력에는 JSON 객체가 두 개 있습니다.

{"name":"홍길동"} {"name":"김철수"}

하나의 요청 body에 JSON 값 하나만 허용하려면 첫 번째 디코딩 후 두 번째 Decodeio.EOF인지 확인합니다.

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "strings"
)

type UserRequest struct {
    Name string `json:"name"`
}

func decodeSingleJSON(input string, destination any) error {
    decoder := json.NewDecoder(strings.NewReader(input))
    decoder.DisallowUnknownFields()

    if err := decoder.Decode(destination); err != nil {
        return fmt.Errorf("JSON 디코딩 실패: %w", err)
    }

    var extra any
    if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
        if err == nil {
            return fmt.Errorf("하나의 JSON 값만 허용됩니다")
        }
        return fmt.Errorf("추가 데이터 확인 실패: %w", err)
    }

    return nil
}

func main() {
    input := `{"name":"홍길동"} {"name":"김철수"}`

    var request UserRequest
    if err := decodeSingleJSON(input, &request); err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("%+v\n", request)
}

출력:

하나의 JSON 값만 허용됩니다

입력 크기 제한

Decoderio.Reader에서 데이터를 계속 읽을 수 있습니다. 신뢰할 수 없는 입력에 크기 제한이 없으면 과도한 메모리나 CPU를 사용할 가능성이 있습니다.

일반적인 io.Reader에는 io.LimitReader를 적용할 수 있습니다.

const maxInputSize = 1 << 20 // 1 MiB

limitedReader := io.LimitReader(reader, maxInputSize+1)

다만 LimitReader만 사용하면 입력이 잘렸는지 명확히 구분해야 합니다. 최대 크기보다 1 byte 더 읽어 실제 초과 여부를 확인하는 방식이 안전합니다.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
)

const maxInputSize = 64

type Request struct {
    Message string `json:"message"`
}

func decodeLimitedJSON(reader io.Reader, destination any) error {
    limited := io.LimitReader(reader, maxInputSize+1)

    data, err := io.ReadAll(limited)
    if err != nil {
        return fmt.Errorf("입력 읽기 실패: %w", err)
    }

    if len(data) > maxInputSize {
        return fmt.Errorf("입력은 최대 %d byte까지 허용됩니다", maxInputSize)
    }

    if err := json.Unmarshal(data, destination); err != nil {
        return fmt.Errorf("JSON 디코딩 실패: %w", err)
    }

    return nil
}

func main() {
    input := []byte(`{"message":"허용된 크기의 메시지"}`)

    var request Request
    if err := decodeLimitedJSON(bytes.NewReader(input), &request); err != nil {
        fmt.Println(err)
        return
    }

    fmt.Printf("%+v\n", request)
}

HTTP 서버에서는 http.MaxBytesReader를 사용해 요청 body 크기를 제한할 수 있습니다.

func handler(w http.ResponseWriter, r *http.Request) {
    const maxBodySize = 1 << 20 // 1 MiB

    r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)
    defer r.Body.Close()

    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields()

    var request CreateProductRequest
    if err := decoder.Decode(&request); err != nil {
        http.Error(w, "잘못된 요청입니다", http.StatusBadRequest)
        return
    }
}

크기 제한은 애플리케이션의 실제 요청 크기와 운영 환경을 기준으로 정해야 합니다.


XML 기본 변환

XML에서는 xml.Name과 XML 전용 struct tag를 사용해 root element, 하위 element, attribute를 지정할 수 있습니다.

package main

import (
    "encoding/xml"
    "fmt"
    "log"
)

type Product struct {
    XMLName xml.Name `xml:"product"`
    ID      int      `xml:"id,attr"`
    Name    string   `xml:"name"`
    Price   int      `xml:"price"`
    Tags    []string `xml:"tags>tag"`
    Internal string  `xml:"-"`
}

func main() {
    product := Product{
        ID:       1001,
        Name:     "키보드",
        Price:    89000,
        Tags:     []string{"입력장치", "USB"},
        Internal: "관리용 메모",
    }

    data, err := xml.MarshalIndent(product, "", "  ")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(xml.Header + string(data))
}

출력:

<?xml version="1.0" encoding="UTF-8"?>
<product id="1001">
  <name>키보드</name>
  <price>89000</price>
  <tags>
    <tag>입력장치</tag>
    <tag>USB</tag>
  </tags>
</product>

XML tag의 의미는 다음과 같습니다.

tag 의미
xml:"product" root element 이름 지정
xml:"id,attr" id를 element가 아닌 attribute로 표현
xml:"tags>tag" <tags> 아래에 반복되는 <tag> element로 표현
xml:"-" XML 변환 대상에서 제외

xml.MarshalIndent는 XML 선언을 자동으로 추가하지 않습니다. XML 선언이 필요하면 xml.Header를 앞에 붙일 수 있습니다.


XML을 구조체로 변환하기

package main

import (
    "encoding/xml"
    "fmt"
    "log"
)

type Product struct {
    XMLName xml.Name `xml:"product"`
    ID      int      `xml:"id,attr"`
    Name    string   `xml:"name"`
    Price   int      `xml:"price"`
    Tags    []string `xml:"tags>tag"`
}

func main() {
    input := []byte(`
        <product id="1001">
            <name>키보드</name>
            <price>89000</price>
            <tags>
                <tag>입력장치</tag>
                <tag>USB</tag>
            </tags>
        </product>
    `)

    var product Product
    if err := xml.Unmarshal(input, &product); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("ID: %d\n", product.ID)
    fmt.Printf("이름: %s\n", product.Name)
    fmt.Printf("가격: %d\n", product.Price)
    fmt.Printf("태그: %v\n", product.Tags)
}

출력:

ID: 1001
이름: 키보드
가격: 89000
태그: [입력장치 USB]

XMLNamexml:"product"를 지정하면 입력의 root element가 <product>인지 확인하는 역할도 합니다. 다른 이름이 들어오면 오류가 발생합니다.


XML struct tag 사용법

encoding/xml은 element와 attribute 외에도 문자 데이터, 중첩 경로 등을 struct tag로 지정할 수 있습니다.

type Article struct {
    XMLName xml.Name `xml:"article"`
    ID      string   `xml:"id,attr"`
    Title   string   `xml:"title"`
    Author  string   `xml:"metadata>author"`
    Content string   `xml:",chardata"`
}

주요 option은 다음과 같습니다.

option 의미
,attr XML attribute로 처리
,chardata element 내부의 문자 데이터 저장
,cdata marshal할 때 CDATA section으로 작성
,innerxml 내부 XML 원문 저장 또는 그대로 출력
,comment XML comment로 처리
a>b>c 중첩된 element 경로 지정
- 변환에서 제외

innerxml은 XML 내용을 구조화하지 않고 원문으로 보관하므로, 신뢰할 수 없는 입력을 다시 출력하거나 다른 문서에 삽입할 때는 사용 목적을 명확히 해야 합니다.


XML의 알 수 없는 element 처리

xml.Unmarshalxml.Decoder는 구조체 필드에 대응되지 않는 element와 attribute를 일반적으로 건너뜁니다.

package main

import (
    "encoding/xml"
    "fmt"
    "log"
)

type User struct {
    XMLName xml.Name `xml:"user"`
    Name    string   `xml:"name"`
}

func main() {
    input := []byte(`
        <user admin="true">
            <name>홍길동</name>
            <role>administrator</role>
        </user>
    `)

    var user User
    if err := xml.Unmarshal(input, &user); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", user)
}

출력에는 Name만 저장됩니다.

{XMLName:{Space: Local:user} Name:홍길동}

encoding/xml에는 JSON의 DisallowUnknownFields와 같은 구조체 전용 option이 없습니다. 알 수 없는 element를 금지해야 한다면 다음 방법을 고려해야 합니다.

  • Decoder.Token으로 element를 직접 검사
  • UnmarshalXML 메서드를 구현해 허용 element를 검증
  • 디코딩 후 별도의 schema 또는 비즈니스 검증 수행

Decoder.Strict는 알 수 없는 element를 금지하는 설정이 아닙니다. 잘못 닫힌 element나 일부 비표준 XML 문법을 얼마나 엄격하게 처리할지 결정하는 설정입니다.


XML stream 처리

xml.Decoderio.Reader에서 XML token이나 element를 순차적으로 읽을 수 있습니다.

package main

import (
    "encoding/xml"
    "fmt"
    "io"
    "log"
    "strings"
)

type Product struct {
    ID   int    `xml:"id,attr"`
    Name string `xml:"name"`
}

func main() {
    input := `
        <products>
            <product id="1001"><name>키보드</name></product>
            <product id="1002"><name>마우스</name></product>
        </products>
    `

    decoder := xml.NewDecoder(strings.NewReader(input))

    for {
        token, err := decoder.Token()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }

        start, ok := token.(xml.StartElement)
        if !ok || start.Name.Local != "product" {
            continue
        }

        var product Product
        if err := decoder.DecodeElement(&product, &start); err != nil {
            log.Fatal(err)
        }

        fmt.Printf("%d: %s\n", product.ID, product.Name)
    }
}

출력:

1001: 키보드
1002: 마우스

문서 전체를 하나의 큰 구조체에 저장하지 않고 필요한 element를 순차적으로 처리할 수 있으므로 큰 XML 문서에 유용합니다. 다만 입력 크기와 element 개수 같은 애플리케이션 수준의 제한은 별도로 적용해야 합니다.


JSON과 XML을 함께 지원하는 구조체

하나의 구조체에 JSON tag와 XML tag를 함께 지정할 수 있습니다.

type Product struct {
    XMLName xml.Name `json:"-" xml:"product"`
    ID      int      `json:"id" xml:"id,attr"`
    Name    string   `json:"name" xml:"name"`
    Price   int      `json:"price" xml:"price"`
    Tags    []string `json:"tags,omitempty" xml:"tags>tag,omitempty"`
}

XMLName은 XML 변환에 필요하지만 JSON에는 필요하지 않으므로 json:"-"로 제외합니다.

package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
    "log"
)

type Product struct {
    XMLName xml.Name `json:"-" xml:"product"`
    ID      int      `json:"id" xml:"id,attr"`
    Name    string   `json:"name" xml:"name"`
    Price   int      `json:"price" xml:"price"`
    Tags    []string `json:"tags,omitempty" xml:"tags>tag,omitempty"`
}

func main() {
    product := Product{
        ID:    1001,
        Name:  "키보드",
        Price: 89000,
        Tags:  []string{"입력장치", "USB"},
    }

    jsonData, err := json.MarshalIndent(product, "", "  ")
    if err != nil {
        log.Fatal(err)
    }

    xmlData, err := xml.MarshalIndent(product, "", "  ")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("JSON")
    fmt.Println(string(jsonData))

    fmt.Println("\nXML")
    fmt.Println(xml.Header + string(xmlData))
}

외부 형식이 다르더라도 내부 도메인 구조가 완전히 같다면 하나의 구조체를 공유할 수 있습니다. 하지만 JSON과 XML의 표현 차이가 커지면 전송 전용 구조체를 각각 분리하는 편이 더 명확할 수 있습니다.


전체 예제

아래 예제는 다음 내용을 한 번에 확인합니다.

  • 구조체의 JSON·XML 변환
  • JSON unknown field 거부
  • UseNumber 사용
  • JSON stream 디코딩
  • XML stream 디코딩

프로젝트를 생성합니다.

mkdir -p sources/json-xml
cd sources/json-xml
go mod init example.com/json-xml
go mod edit -go=1.26.1

main.go:

package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
    "io"
    "log"
    "strings"
)

type Product struct {
    XMLName xml.Name `json:"-" xml:"product"`
    ID      int64    `json:"id" xml:"id,attr"`
    Name    string   `json:"name" xml:"name"`
    Price   int64    `json:"price" xml:"price"`
    Tags    []string `json:"tags,omitempty" xml:"tags>tag,omitempty"`
    Internal string  `json:"-" xml:"-"`
}

type Event struct {
    Type string `json:"type"`
    ID   int64  `json:"id"`
}

func main() {
    product := Product{
        ID:       1001,
        Name:     "키보드",
        Price:    89000,
        Tags:     []string{"입력장치", "USB"},
        Internal: "외부에 출력하지 않는 값",
    }

    printJSON(product)
    printXML(product)
    decodeStrictJSON()
    decodeJSONNumber()
    decodeJSONStream()
    decodeXMLStream()
}

func printJSON(product Product) {
    data, err := json.MarshalIndent(product, "", "  ")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("=== JSON Marshal ===")
    fmt.Println(string(data))
}

func printXML(product Product) {
    data, err := xml.MarshalIndent(product, "", "  ")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("\n=== XML Marshal ===")
    fmt.Println(xml.Header + string(data))
}

func decodeStrictJSON() {
    input := `{"id":1001,"name":"키보드","price":89000,"unknown":true}`

    decoder := json.NewDecoder(strings.NewReader(input))
    decoder.DisallowUnknownFields()

    var product Product
    err := decoder.Decode(&product)

    fmt.Println("\n=== JSON Unknown Field ===")
    if err != nil {
        fmt.Println("오류:", err)
        return
    }

    fmt.Printf("%+v\n", product)
}

func decodeJSONNumber() {
    input := `{"id":9007199254740993}`

    decoder := json.NewDecoder(strings.NewReader(input))
    decoder.UseNumber()

    var value map[string]any
    if err := decoder.Decode(&value); err != nil {
        log.Fatal(err)
    }

    number, ok := value["id"].(json.Number)
    if !ok {
        log.Fatal("id가 json.Number가 아닙니다")
    }

    id, err := number.Int64()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("\n=== JSON Number ===")
    fmt.Printf("표현: %s, int64: %d, 타입: %T\n", number, id, value["id"])
}

func decodeJSONStream() {
    input := `
        {"type":"created","id":1001}
        {"type":"updated","id":1002}
    `

    decoder := json.NewDecoder(strings.NewReader(input))

    fmt.Println("\n=== JSON Stream ===")

    for {
        var event Event
        err := decoder.Decode(&event)

        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }

        fmt.Printf("%s: %d\n", event.Type, event.ID)
    }
}

func decodeXMLStream() {
    input := `
        <products>
            <product id="1001"><name>키보드</name><price>89000</price></product>
            <product id="1002"><name>마우스</name><price>45000</price></product>
        </products>
    `

    decoder := xml.NewDecoder(strings.NewReader(input))

    fmt.Println("\n=== XML Stream ===")

    for {
        token, err := decoder.Token()

        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }

        start, ok := token.(xml.StartElement)
        if !ok || start.Name.Local != "product" {
            continue
        }

        var product Product
        if err := decoder.DecodeElement(&product, &start); err != nil {
            log.Fatal(err)
        }

        fmt.Printf("%d: %s, %d원\n", product.ID, product.Name, product.Price)
    }
}

실행합니다.

cd sources
go run ./json-xml

출력 예:

=== JSON Marshal ===
{
  "id": 1001,
  "name": "키보드",
  "price": 89000,
  "tags": [
    "입력장치",
    "USB"
  ]
}

=== XML Marshal ===
<?xml version="1.0" encoding="UTF-8"?>
<product id="1001">
  <name>키보드</name>
  <price>89000</price>
  <tags>
    <tag>입력장치</tag>
    <tag>USB</tag>
  </tags>
</product>

=== JSON Unknown Field ===
오류: json: unknown field "unknown"

=== JSON Number ===
표현: 9007199254740993, int64: 9007199254740993, 타입: json.Number

=== JSON Stream ===
created: 1001
updated: 1002

=== XML Stream ===
1001: 키보드, 89000원
1002: 마우스, 45000원

적용 기준

JSON이나 XML을 처리할 때는 다음 기준으로 선택할 수 있습니다.

상황 권장 방식
입력 구조를 알고 있음 구체적인 구조체로 decode
동적인 JSON 구조 map[string]any, 필요하면 UseNumber 적용
완전한 []byte 데이터 Marshal·Unmarshal
HTTP body, 파일, 표준 입력 Encoder·Decoder
JSON API에서 오타 필드 거부 DisallowUnknownFields
요청 body 처리 크기 제한과 단일 값 여부 확인
큰 XML 문서의 일부 element 처리 Decoder.Token·DecodeElement
JSON과 XML 표현 구조가 크게 다름 전송 전용 구조체 분리

구조체로 디코딩하면 필드 타입이 명확해지고, 숫자 변환과 type assertion을 줄일 수 있습니다. 외부 요청을 받을 때는 단순히 디코딩 성공 여부만 확인하지 말고 필수값, 값 범위, 문자열 길이 같은 비즈니스 검증도 추가해야 합니다.


정리

encoding/jsonencoding/xml은 Go 표준 라이브러리에서 JSON과 XML을 처리하는 기본 패키지입니다.

두 패키지는 exported field를 중심으로 구조체를 변환하며, struct tag를 사용해 외부 필드 이름과 출력 방법을 지정합니다.

JSON 구조를 알고 있다면 map[string]any보다 명확한 구조체로 디코딩하는 것이 좋습니다. any로 JSON 숫자를 받으면 기본적으로 float64가 되므로 숫자의 정확한 표현이 필요하면 구조체의 정수 타입이나 Decoder.UseNumber를 사용해야 합니다.

완전한 byte slice에는 MarshalUnmarshal이 간단하고, HTTP body나 파일 같은 stream에는 EncoderDecoder가 적합합니다.

신뢰할 수 없는 입력을 처리할 때는 다음 사항을 함께 적용하는 것이 중요합니다.

  • 입력 크기 제한
  • JSON unknown field 정책
  • 하나의 문서만 허용할지 여부
  • 필수값과 값 범위 검증
  • XML에서 허용할 element와 attribute 검증

디코딩은 외부 문서를 Go 값으로 옮기는 과정일 뿐이며, 해당 값이 애플리케이션에서 유효하고 안전한지는 별도의 검증 단계에서 확인해야 합니다.


참고 자료

반응형

댓글