JSON Marshal and Unmarshal in Go
Last updated:
Go's standard library encoding/json package provides everything you need to work with JSON: marshaling (Go value to JSON bytes), unmarshaling (JSON bytes to Go value), struct tags for field control, and streaming codecs for large payloads. This guide covers each feature in depth with runnable examples.
Glossary
- Marshaling
- Converting a Go value (struct, map, slice, primitive) into its JSON byte representation. Called via
json.Marshal(v)orjson.NewEncoder(w).Encode(v). - Unmarshaling
- Parsing JSON bytes and populating a Go value. Called via
json.Unmarshal(data, &v)orjson.NewDecoder(r).Decode(&v). Always pass a pointer so the value can be modified. - Struct tag
- A raw string literal in backticks after a struct field declaration that provides metadata to reflection-based libraries. The
jsonkey controls encoding/json behavior: field name, omitempty, and string coercion. - RawMessage
- A
[]bytetype alias (type RawMessage []byte) in theencoding/jsonpackage. It passes raw JSON bytes through Marshal/Unmarshal untouched, enabling deferred or conditional decoding. - Zero value
- Go's default value for each type:
0for integers,0.0for floats,falsefor booleans,""for strings,nilfor pointers/slices/maps. Theomitemptytag option omits a field if it holds its zero value.
Marshaling Structs to JSON
json.Marshal converts a Go value to a JSON-encoded []byte. Exported struct fields are included by default; their JSON keys default to the exact field name.
package main
import (
"encoding/json"
"fmt"
"log"
)
type Address struct {
Street string
City string
Zip string
}
type User struct {
ID int
Name string
Email string
Address Address
}
func main() {
u := User{
ID: 42,
Name: "Ada Lovelace",
Email: "ada@example.com",
Address: Address{
Street: "123 Main St",
City: "London",
Zip: "SW1A 1AA",
},
}
data, err := json.Marshal(u)
if err != nil {
log.Fatalf("marshal error: %v", err)
}
fmt.Println(string(data))
// {"ID":42,"Name":"Ada Lovelace","Email":"ada@example.com",
// "Address":{"Street":"123 Main St","City":"London","Zip":"SW1A 1AA"}}
// Pretty-print with indentation
pretty, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(pretty))
}json.MarshalIndent(v, prefix, indent) adds whitespace for human-readable output. Use it for configuration files, debugging, or API responses where readability matters. For high-throughput APIs, prefer compact json.Marshal output.
Unmarshaling JSON to Structs
json.Unmarshal parses JSON bytes into a Go value. Always pass a pointer — otherwise the function cannot modify the original value.
package main
import (
"encoding/json"
"fmt"
"log"
)
type Product struct {
ID int
Name string
Price float64
InStock bool
Tags []string
}
func main() {
raw := []byte(`{
"ID": 7,
"Name": "Gopher Plushie",
"Price": 19.99,
"InStock": true,
"Tags": ["toys", "go", "merch"]
}`)
var p Product
if err := json.Unmarshal(raw, &p); err != nil {
log.Fatalf("unmarshal error: %v", err)
}
fmt.Printf("Product: %+v
", p)
// Product: {ID:7 Name:Gopher Plushie Price:19.99 InStock:true Tags:[toys go merch]}
fmt.Printf("First tag: %s
", p.Tags[0]) // toys
}Unknown JSON fields are silently ignored by default. Missing JSON fields leave the corresponding Go struct field at its zero value. To disallow unknown fields, use a json.Decoder with DisallowUnknownFields().
import (
"bytes"
"encoding/json"
"fmt"
)
func strictUnmarshal(data []byte, v any) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
return dec.Decode(v)
}
// Now an unknown field like "colour" will return an error:
// json: unknown field "colour"Struct Tags: json:"name,omitempty"
Struct tags are the primary way to control how encoding/json handles each field: renaming keys, skipping zero values, and coercing types.
type Article struct {
// Rename: JSON key is "id", not "ID"
ID int `json:"id"`
// Rename + omitempty: omit if Title == ""
Title string `json:"title,omitempty"`
// Completely skip this field in both marshal and unmarshal
Internal string `json:"-"`
// string option: encode int as a JSON string "42" instead of 42
ViewCount int `json:"view_count,string"`
// Pointer: distinguishes null from missing
DeletedAt *string `json:"deleted_at,omitempty"`
// Unexported — always ignored regardless of tags
secret string
}
// Marshal example
a := Article{ID: 1, Title: "Go JSON", ViewCount: 100}
data, _ := json.Marshal(a)
// {"id":1,"title":"Go JSON","view_count":"100"}
// Note: Title present (non-zero), deleted_at absent (nil pointer + omitempty)
// Empty Title with omitempty
a2 := Article{ID: 2, ViewCount: 50}
data2, _ := json.Marshal(a2)
// {"id":2,"view_count":"50"}
// Title omitted because it's "" (zero value)omitempty and Zero Values
The omitempty option omits a field if it equals its zero value. For pointers and slices, the zero value is nil. An empty slice ([]string{}) is not nil — it will still be included. Use a nil slice if you want omission; use an empty slice if you want [] in the output.
type Config struct {
Timeout int `json:"timeout,omitempty"` // omitted if 0
Debug bool `json:"debug,omitempty"` // omitted if false
Tags []string `json:"tags,omitempty"` // omitted if nil, NOT if []string{}
}
// nil slice → omitted
c1 := Config{Timeout: 30}
data1, _ := json.Marshal(c1)
// {"timeout":30} — debug and tags omitted
// empty slice → NOT omitted
c2 := Config{Tags: []string{}}
data2, _ := json.Marshal(c2)
// {"tags":[]} — timeout and debug omitted, tags included as []Dynamic JSON with interface{} and map[string]interface{}
When you don't know the JSON structure at compile time, unmarshal into interface{} or map[string]interface{}. The JSON decoder maps JSON types to Go types automatically.
import (
"encoding/json"
"fmt"
)
func main() {
raw := []byte(`{"name":"Gopher","age":5,"active":true,"scores":[10,20,30]}`)
// Unmarshal into interface{}
var v interface{}
json.Unmarshal(raw, &v)
// Type assert to map
m := v.(map[string]interface{})
fmt.Println(m["name"]) // Gopher (string)
fmt.Println(m["age"]) // 5 (float64 — all JSON numbers become float64)
fmt.Println(m["active"]) // true (bool)
scores := m["scores"].([]interface{})
for _, s := range scores {
fmt.Printf("%.0f ", s.(float64)) // 10 20 30
}
// json.Number preserves numeric precision
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber()
var v2 interface{}
dec.Decode(&v2)
m2 := v2.(map[string]interface{})
age := m2["age"].(json.Number)
fmt.Println(age.String()) // "5" — no float64 precision loss
n, _ := age.Int64()
fmt.Println(n) // 5
}The key caveat: all JSON numbers become float64 when decoded into interface{}. For large integers (ID fields, timestamps), use dec.UseNumber() to preserve the original string representation via json.Number.
json.RawMessage for Deferred Decoding
json.RawMessage holds raw JSON bytes, passing them through marshaling and unmarshaling unchanged. This is the standard Go pattern for polymorphic payloads.
package main
import (
"encoding/json"
"fmt"
"log"
)
// An event envelope where payload varies by type
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
type ClickEvent struct {
X, Y int
Target string
}
type FormEvent struct {
FieldName string
Value string
}
func processEvent(data []byte) {
var e Event
if err := json.Unmarshal(data, &e); err != nil {
log.Fatal(err)
}
switch e.Type {
case "click":
var click ClickEvent
json.Unmarshal(e.Payload, &click)
fmt.Printf("Click at (%d,%d) on %s
", click.X, click.Y, click.Target)
case "form":
var form FormEvent
json.Unmarshal(e.Payload, &form)
fmt.Printf("Form: %s = %s
", form.FieldName, form.Value)
default:
fmt.Printf("Unknown event type: %s, raw payload: %s
", e.Type, e.Payload)
}
}
func main() {
processEvent([]byte(`{"type":"click","payload":{"X":100,"Y":200,"Target":"button#submit"}}`))
processEvent([]byte(`{"type":"form","payload":{"FieldName":"email","Value":"user@example.com"}}`))
}json.RawMessage also works in marshaling — embed it in a struct to inject pre-encoded JSON directly without re-encoding. This is efficient when you already have valid JSON bytes from a cache or upstream service.
// Embedding pre-encoded JSON into a response
cachedPayload := json.RawMessage(`{"items":[1,2,3],"total":3}`)
resp := struct {
Status string `json:"status"`
Data json.RawMessage `json:"data"`
}{
Status: "ok",
Data: cachedPayload,
}
out, _ := json.Marshal(resp)
fmt.Println(string(out))
// {"status":"ok","data":{"items":[1,2,3],"total":3}}Streaming with json.Encoder and json.Decoder
json.NewEncoder(w).Encode(v) writes JSON directly to an io.Writer. json.NewDecoder(r).Decode(&v) reads from an io.Reader. Both stream data without requiring the full payload in memory.
package main
import (
"encoding/json"
"net/http"
)
type Response struct {
Status string `json:"status"`
Data any `json:"data"`
}
// HTTP handler using Encoder — writes directly to ResponseWriter
func handleGet(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ") // optional pretty-print
if err := enc.Encode(Response{Status: "ok", Data: map[string]int{"count": 42}}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// HTTP handler using Decoder — reads from Request.Body
type CreateRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
func handlePost(w http.ResponseWriter, r *http.Request) {
var req CreateRequest
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
// use req.Name, req.Email ...
json.NewEncoder(w).Encode(Response{Status: "created", Data: req})
}Decoding a JSON Array Stream
For large JSON arrays (log files, data exports), decode element by element to keep memory usage flat:
package main
import (
"encoding/json"
"fmt"
"io"
"os"
)
type LogEntry struct {
Level string `json:"level"`
Message string `json:"message"`
Time string `json:"time"`
}
func processLargeFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
dec := json.NewDecoder(f)
// Read opening bracket of the array
if _, err := dec.Token(); err != nil {
return err
}
count := 0
for dec.More() {
var entry LogEntry
if err := dec.Decode(&entry); err != nil {
return fmt.Errorf("decode entry %d: %w", count, err)
}
// Process entry — only one entry in memory at a time
if entry.Level == "error" {
fmt.Println("ERROR:", entry.Message)
}
count++
}
fmt.Printf("Processed %d entries
", count)
return nil
}Custom MarshalJSON and UnmarshalJSON
Implement the json.Marshaler and json.Unmarshaler interfaces to fully control how a type encodes and decodes. Common use cases: custom time formats, enums as strings, field transformations, and flattening nested structs.
package main
import (
"encoding/json"
"fmt"
"time"
)
// Custom time type using a non-standard format
type CustomTime struct {
time.Time
}
const customFormat = "02 Jan 2006"
func (ct CustomTime) MarshalJSON() ([]byte, error) {
return json.Marshal(ct.Time.Format(customFormat))
}
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
t, err := time.Parse(customFormat, s)
if err != nil {
return err
}
ct.Time = t
return nil
}
type Event struct {
Name string `json:"name"`
CreatedAt CustomTime `json:"created_at"`
}
func main() {
e := Event{
Name: "Launch",
CreatedAt: CustomTime{time.Date(2026, 5, 19, 0, 0, 0, 0, time.UTC)},
}
data, _ := json.Marshal(e)
fmt.Println(string(data))
// {"name":"Launch","created_at":"19 May 2026"}
var e2 Event
json.Unmarshal(data, &e2)
fmt.Println(e2.CreatedAt.Format("2006-01-02")) // 2026-05-19
}Enum Marshaling
type Status int
const (
StatusPending Status = iota
StatusActive
StatusClosed
)
var statusNames = map[Status]string{
StatusPending: "pending",
StatusActive: "active",
StatusClosed: "closed",
}
var statusValues = map[string]Status{
"pending": StatusPending,
"active": StatusActive,
"closed": StatusClosed,
}
func (s Status) MarshalJSON() ([]byte, error) {
name, ok := statusNames[s]
if !ok {
return nil, fmt.Errorf("unknown status: %d", s)
}
return json.Marshal(name)
}
func (s *Status) UnmarshalJSON(data []byte) error {
var name string
if err := json.Unmarshal(data, &name); err != nil {
return err
}
val, ok := statusValues[name]
if !ok {
return fmt.Errorf("unknown status: %q", name)
}
*s = val
return nil
}
// Marshal: StatusActive → "active"
// Unmarshal: "closed" → StatusClosedFlattening Embedded Structs
// Using inline tag to flatten nested struct fields
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}
type Person struct {
Name string `json:"name"`
Address `json:",inline"` // not supported natively — use embedded struct
}
// Go encoding/json DOES support anonymous (embedded) struct flattening:
type PersonFlat struct {
Name string `json:"name"`
Address // embedded — fields promoted to top level in JSON
}
p := PersonFlat{Name: "Ada", Address: Address{Street: "123 Main", City: "London"}}
data, _ := json.Marshal(p)
fmt.Println(string(data))
// {"name":"Ada","street":"123 Main","city":"London"}FAQ
What does json.Marshal return in Go?
json.Marshal returns ([]byte, error). The []byte is the JSON-encoded output. The error is non-nil only for types that can't be marshaled: channels, functions, cyclic structures, and custom MarshalJSON implementations that return errors. Always check the error — even if you don't expect it, defensive error handling is idiomatic Go.
How do json struct tags work in Go?
Struct tags are backtick string literals after a field declaration. The json key controls encoding: the first value is the JSON key name, followed by optional comma-separated options. omitempty skips the field when it's a zero value. string encodes numbers and booleans as JSON strings. Use - to completely skip a field. Tags have no impact on unexported fields — those are always ignored.
Why are unexported struct fields ignored by encoding/json?
Go's reflection system enforces package-level access control. Fields beginning with a lowercase letter are unexported — they're not accessible via reflect.Value.Field from outside the defining package. encoding/json uses reflection to enumerate and access struct fields, so it simply cannot see unexported fields. This is intentional: it prevents packages from accidentally leaking internal state through JSON serialization.
What is json.RawMessage used for in Go?
json.RawMessage stores raw JSON bytes and passes them through marshaling/unmarshaling unchanged. It's the idiomatic solution for polymorphic payloads — unmarshal the envelope to read a discriminator field, then unmarshal the RawMessage into the appropriate concrete type. It also lets you inject pre-encoded JSON (from a cache, for example) into a struct without re-encoding.
When should I use json.Decoder instead of json.Unmarshal?
Use json.Decoder when the JSON comes from an io.Reader (HTTP request body, file, network connection) — it avoids reading the entire payload into a []byte first. Use it also when processing a stream of multiple JSON values (newline-delimited JSON), or when iterating over large arrays element by element with dec.More() and dec.Decode. For in-memory []byte, json.Unmarshal is simpler and slightly faster.
How do I implement custom JSON marshaling in Go?
Implement MarshalJSON() ([]byte, error) on your type for custom encoding, and UnmarshalJSON([]byte) error for custom decoding. These interfaces (json.Marshaler and json.Unmarshaler) are invoked automatically. For UnmarshalJSON, use a pointer receiver so the method can modify the receiver. A common pattern is to define a shadow struct or alias type inside the method to avoid infinite recursion when calling json.Marshal internally.
How do I marshal a Go map to JSON?
json.Marshal supports map[string]T for any marshallable T. Map keys must be strings, integers, or implement encoding.TextMarshaler. Map keys are sorted lexicographically in the output (deterministic ordering since Go 1.12). For unknown structures, unmarshal into map[string]interface{} — but remember all numbers become float64. Use dec.UseNumber() to get json.Number instead.
How do I handle null JSON values in Go structs?
Use pointer types (*string, *int, etc.) to represent nullable fields. A nil pointer marshals to JSON null; a non-nil pointer marshals to its pointed-to value. For unmarshaling, a JSON null sets the pointer to nil. Without a pointer, you cannot distinguish between "field was null" and "field was 0 / empty string" — both produce the same zero value. Combine pointer types with omitempty to omit nil fields entirely.
Further reading and primary sources
- Go encoding/json — Official Go encoding/json package documentation
- JSON and Go (Go Blog) — The Go Blog tutorial on JSON encoding and decoding
- go-json: Fast JSON for Go — Drop-in replacement for encoding/json with 3x performance improvement