Decode JSON with no fixed struct behind it, using map[string]any when the document is free-form, and json.RawMessage to defer part of one.
Unmarshalling into map[string]any accepts any object. Every value comes back boxed in an any, so reading one means a type assertion.
Use the two-result form. The single-result form panics when the key is missing or holds another type.
var v map[string]any
if err := json.Unmarshal(data, &v); err != nil {
// handle error
}
name, ok := v["name"].(string)
if !ok {
// key missing, or not a string
}Every JSON number lands as a float64, including one written without a decimal point. Asserting int on it fails.
Past 2^53 a float64 starts losing whole-number precision, which shows up with 64-bit database IDs and Twitter-style snowflake IDs.
// What every JSON value becomes in an any:
// object -> map[string]any
// array -> []any
// string -> string
// number -> float64
// true -> bool
// null -> nil
port, ok := v["port"].(float64) // not int
if ok {
fmt.Println(int(port))
}Deferring part of a document
json.RawMessage stores the original bytes for that key without parsing them. The outer document decodes normally, and the deferred part waits until a discriminator field says what it is.
This is the usual shape for webhook payloads and event feeds, where one endpoint carries several message types.
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
var e Event
json.Unmarshal(data, &e)
switch e.Type {
case "signup":
var p SignupPayload
if err := json.Unmarshal(e.Payload, &p); err != nil {
// handle error
}
case "purchase":
var p PurchasePayload
json.Unmarshal(e.Payload, &p)
}UseNumber keeps numbers as the literal text that arrived, wrapped in a json.Number. Int64 then converts exactly, or reports that it cannot.
dec := json.NewDecoder(bytes.NewReader(data))
dec.UseNumber()
var v map[string]any
dec.Decode(&v)
id := v["id"].(json.Number)
exact, err := id.Int64() // no float64 roundingWorking program
A complete program that reads a two-event feed, dispatching each payload on the type field.
package main
import (
"encoding/json"
"fmt"
)
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
type Signup struct {
Email string `json:"email"`
}
type Purchase struct {
SKU string `json:"sku"`
Cents int `json:"cents"`
}
func describe(raw []byte) (string, error) {
var e Event
if err := json.Unmarshal(raw, &e); err != nil {
return "", err
}
switch e.Type {
case "signup":
var p Signup
if err := json.Unmarshal(e.Payload, &p); err != nil {
return "", err
}
return "signup from " + p.Email, nil
case "purchase":
var p Purchase
if err := json.Unmarshal(e.Payload, &p); err != nil {
return "", err
}
return fmt.Sprintf("purchase %s for %d cents", p.SKU, p.Cents), nil
}
return "", fmt.Errorf("unknown event type %q", e.Type)
}
func main() {
feed := [][]byte{
[]byte(`{"type":"signup","payload":{"email":"kt@example.com"}}`),
[]byte(`{"type":"purchase","payload":{"sku":"GO-1","cents":2500}}`),
}
for _, raw := range feed {
s, err := describe(raw)
if err != nil {
fmt.Println("skipped:", err)
continue
}
fmt.Println(s)
}
}Run it.
$ go run main.go
signup from kt@example.com
purchase GO-1 for 2500 cents