Mapping JSON to Go types

A recurring theme in Go JSON questions is how to map a piece of JSON data onto Go types. The data is usually an API response, but the pattern applies to any nested JSON structure. Working with a simple fruit-inventory example, three representation strategies stand out, each with its own trade-offs.

Fully typed structs

The default choice for most cases is a complete, type-safe mapping from JSON to a Go struct. Tools like JSON-to-Go can generate the struct declarations for you:

type AutoGenerated struct {
  Attrs []struct {
    Name  string `json:"name"`
    Count int    `json:"count"`
  } `json:"attrs"`
  Fruits []struct {
    Name      string  `json:"name"`
    Sweetness float64 `json:"sweetness"`
    Attr      struct {
      Family string `json:"family"`
    } `json:"attr"`
  } `json:"fruits"`
}

Parsing into this struct and iterating over the fruits is straightforward and fully compile-time checked:

var ag AutoGenerated
if err := json.Unmarshal(jsonText, &ag); err != nil {
  log.Fatal(err)
}
for _, fruit := range ag.Fruits {
  fmt.Printf("%s -> %f\n", fruit.Name, fruit.Sweetness)
}

This approach is barely more verbose than what you'd write in a dynamic language, but it validates the JSON structure automatically during parsing and gives you concrete types throughout your code.

Generic maps of interfaces

When you don't want to declare structs upfront, json.Unmarshal can parse into a generic map[string]interface{}. The parser builds concrete types based on what it encounters, but the compiler can't know those types statically, so runtime type assertions are required to traverse the data:

var m map[string]interface{}
if err := json.Unmarshal(jsonText, &m); err != nil {
  log.Fatal(err)
}

fruits, ok := m["fruits"]
if !ok {
  log.Fatal("'fruits' field not found")
}
fslice, ok := fruits.([]interface{})
if !ok {
  log.Fatal("'fruits' field not a slice")
}

for _, f := range fslice {
  fmap, ok := f.(map[string]interface{})
  if !ok {
    log.Fatal("'fruits' element not a map")
  }

  name, ok := fmap["name"]
  if !ok {
    log.Fatal("fruits element has no 'name' field")
  }
  sweetness, ok := fmap["sweetness"]
  if !ok {
    log.Fatal("fruits element has no 'sweetness' field")
  }

  fmt.Printf("%s -> %f\n", name, sweetness)
}

The verbosity here comes from the explicit error checks — field existence and type assertions on every step — not from the untyped nature of the data. The same logic can be written more densely by skipping checks and relying on panics:

var m map[string]interface{}
if err := json.Unmarshal(jsonText, &m); err != nil {
  log.Fatal(err)
}

fruits := m["fruits"].([]interface{})
for _, f := range fruits {
  fruit := f.(map[string]interface{})
  fmt.Printf("%s -> %f\n", fruit["name"], fruit["sweetness"])
}

That variant allows recover at a higher level to handle failures, which is similar to exceptions in dynamic languages, but it forfeits graceful degradation and is generally not idiomatic Go.

Hybrid untyped-until-needed

Between the two extremes lies a compromise: parse generically until you reach the part you care about, then switch to a concrete struct. For the fruit example, define the relevant struct:

type Fruit struct {
  Name      string            `json:"name"`
  Sweetness float64           `json:"sweetness"`
  Attr      map[string]string `json:"attr"`
}

and parse with json.RawMessage delaying the inner parse:

var m map[string]json.RawMessage
if err := json.Unmarshal(jsonText, &m); err != nil {
  log.Fatal(err)
}

fruitsRaw, ok := m["fruits"]
if !ok {
  log.Fatal("expected to find 'fruits'")
}

var fruits []Fruit
if err := json.Unmarshal(fruitsRaw, &fruits); err != nil {
  log.Fatal(err)
}
for _, fruit := range fruits {
  fmt.Printf("%s -> %f\n", fruit.Name, fruit.Sweetness)
}

The key trick is telling json.Unmarshal to store values as json.RawMessage instead of interface{}. The latter forces the parser to build concrete maps and slices eagerly; the former leaves the raw JSON untouched so you can parse it later into a known type like Fruit.

This delayed parsing has real benefits. For a large response where you only need one field, the parser doesn't spend time decoding the values you'll never inspect. And when a schema uses polymorphic fields — where the type of one field depends on another — a fully static struct may be infeasible; the hybrid approach lets you keep an unstructured outer layer while exposing the interesting sub-structures as safe, typed values.

[1]Here and elsewhere throughout the post, log.Fatal is just a placeholder for real error handling.