HowtoGo
Home / How to Go / Parse JSON into a struct
How to Go

Parse JSON into a struct

Turn JSON bytes into a Go struct with json.Unmarshal, and control the mapping with struct tags.

json.Unmarshal takes the bytes and a pointer to the value to fill. Passing the value itself leaves it untouched, so the & matters.

A field only gets filled if it is exported. Matching is case-insensitive when no tag is present.

type Config struct {
    Host    string `json:"host"`
    Port    int    `json:"port"`
    Debug   bool   `json:"debug"`
}

data, err := os.ReadFile("config.json")
if err != nil {
    // handle error
}

var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
    // handle error
}

The tag names the JSON key. omitempty drops the field on the way out when it holds its zero value, and - excludes it in both directions.

Keys in the JSON with no matching field are discarded without complaint.

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"full_name"`
    Nickname string `json:"nickname,omitempty"`
    Internal string `json:"-"`
    password string // unexported, never touched by encoding/json
}

Telling a missing field from a zero one

A missing key and a key set to 0 both leave an int field at zero, which hides the difference between "unset" and "set to nothing".

A pointer field stays nil when the key is absent.

type Settings struct {
    Retries *int `json:"retries"`
}

var s Settings
json.Unmarshal([]byte(`{}`), &s)
// s.Retries == nil, the key was absent

json.Unmarshal([]byte(`{"retries": 0}`), &s)
// *s.Retries == 0, the key was present and zero

A typo in a config key is silently ignored by default. DisallowUnknownFields on a json.Decoder turns it into an error naming the offending key.

dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()

var cfg Config
if err := dec.Decode(&cfg); err != nil {
    // json: unknown field "prot"
}

Working program

A complete program that writes a config file, reads it back into a struct, and wraps any parse failure with the file name.

package main

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

type Config struct {
    Host    string   `json:"host"`
    Port    int      `json:"port"`
    Origins []string `json:"allowed_origins"`
}

func loadConfig(path string) (Config, error) {
    var cfg Config

    data, err := os.ReadFile(path)
    if err != nil {
        return cfg, err
    }
    if err := json.Unmarshal(data, &cfg); err != nil {
        return cfg, fmt.Errorf("parsing %s: %w", path, err)
    }
    return cfg, nil
}

func main() {
    os.WriteFile("config.json", []byte(`{
  "host": "0.0.0.0",
  "port": 8080,
  "allowed_origins": ["https://howtogo.dev"]
}`), 0644)

    cfg, err := loadConfig("config.json")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("%s:%d origins=%v\n", cfg.Host, cfg.Port, cfg.Origins)
}

Run it.

Terminal
$ go run main.go
0.0.0.0:8080 origins=[https://howtogo.dev]