Parse JSON in Go: Complete encoding/json Guide
Go's standard library encoding/json package handles all JSON parsing without any third-party dependency. You decode JSON with json.Unmarshal for in-memory bytes or json.NewDecoder for streaming — Go maps JSON objects to structs automatically using struct field tags.
Need to validate JSON output? Use Jsonic's JSON Validator to check your JSON while testing your Go code.
Validate JSONQuick start: json.Unmarshal
The simplest way to parse JSON in Go is json.Unmarshal. Define a struct whose fields match the JSON keys, then pass a []byte slice and a pointer to the struct. Go fills in the fields automatically.
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
data := []byte(`{"id":1,"name":"Alice","email":"alice@example.com"}`)
var user User
if err := json.Unmarshal(data, &user); err != nil {
log.Fatal(err)
}
fmt.Println(user.Name) // Alice
fmt.Println(user.ID) // 1
fmt.Println(user.Email) // alice@example.com
}json.Unmarshal takes a []byte slice and a pointer to the target value. The function returns an error if the JSON is malformed or incompatible with the target type. Always check the error — silent failures lead to zero-value fields that are hard to debug.
Struct field tags
Go struct field tags control how encoding/json maps JSON keys to struct fields. The tag format is `json:"key,options"`. Without a tag, the package matches JSON keys to exported field names in a case-insensitive manner — but explicit tags are always clearer.
| Tag | Effect | Example |
|---|---|---|
json:"name" | Maps struct field to JSON key "name" | Name string `json:"name"` |
json:"name,omitempty" | Omit field if zero value during marshaling | Age int `json:"age,omitempty"` |
json:"-" | Always skip this field | Password string `json:"-"` |
json:"name,string" | Marshal/unmarshal numeric field as a JSON string | ID int `json:"id,string"` |
Here is a struct that demonstrates all four tag patterns together:
type Profile struct {
ID int `json:"id,string"` // stored as "123" in JSON
Username string `json:"username"`
Bio string `json:"bio,omitempty"` // skipped if empty string
Password string `json:"-"` // never included in JSON
CreatedAt string `json:"created_at"`
}
// Unmarshal example
data := []byte(`{"id":"42","username":"alice","created_at":"2024-01-01"}`)
var p Profile
json.Unmarshal(data, &p)
// p.ID == 42, p.Bio == "", p.Password == ""The omitempty option only applies during marshaling (encoding Go to JSON). During unmarshaling, missing JSON keys simply leave the struct field at its zero value.
Parse JSON from a file with json.NewDecoder
When reading JSON from a file or an HTTP response body, prefer json.NewDecoder over json.Unmarshal. The decoder wraps any io.Reader and streams the data without loading the entire payload into memory first — important when handling large files or long-lived HTTP connections.
package main
import (
"encoding/json"
"log"
"os"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
f, err := os.Open("users.json")
if err != nil {
log.Fatal(err)
}
defer f.Close()
var users []User
decoder := json.NewDecoder(f)
if err := decoder.Decode(&users); err != nil {
log.Fatal(err)
}
for _, u := range users {
log.Printf("User %d: %s (%s)", u.ID, u.Name, u.Email)
}
}The same pattern works for HTTP response bodies — replace os.Open with the resp.Body from http.Get. Remember to defer resp.Body.Close() to release the connection.
// Parsing an HTTP JSON response
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
var users []User
if err := json.NewDecoder(resp.Body).Decode(&users); err != nil {
log.Fatal(err)
}You can also call decoder.Decode in a loop to read a stream of newline-delimited JSON objects (NDJSON), calling until err == io.EOF.
Parse JSON arrays
A JSON array maps directly to a Go slice. Pass a pointer to a slice of the appropriate type — encoding/json allocates the slice elements automatically.
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
data := []byte(`[
{"id":1,"name":"Alice"},
{"id":2,"name":"Bob"},
{"id":3,"name":"Carol"}
]`)
var users []User
if err := json.Unmarshal(data, &users); err != nil {
log.Fatal(err)
}
fmt.Println(len(users)) // 3
fmt.Println(users[0].Name) // Alice
fmt.Println(users[2].Name) // Carol
}Arrays of primitives work the same way. A JSON array of strings becomes []string, an array of numbers becomes []float64 (when decoding into interface) or the typed slice you specify.
// Array of strings
var tags []string
json.Unmarshal([]byte(`["go","json","api"]`), &tags)
// tags == []string{"go", "json", "api"}
// Nested arrays
var matrix [][]int
json.Unmarshal([]byte(`[[1,2],[3,4],[5,6]]`), &matrix)
// matrix[1][0] == 3Dynamic / unknown JSON with map and interface
When the JSON structure is not known at compile time — for example, when proxying an API or reading config files with variable shapes — decode into map[string]interface. Go will build a tree of maps, slices, strings, floats, booleans, and nils that mirrors the JSON exactly.
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
data := []byte(`{
"name": "Alice",
"age": 30,
"active": true,
"score": 9.5
}`)
var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
log.Fatal(err)
}
// Type assertions to read values
name := result["name"].(string)
age := result["age"].(float64) // JSON numbers are always float64 here
active := result["active"].(bool)
fmt.Println(name) // Alice
fmt.Println(int(age)) // 30
fmt.Println(active) // true
}All JSON numbers decode to float64 when the target is interface. Convert to int explicitly or use json.Number with a custom decoder to preserve the original string representation.
// Use json.Number to avoid float64 precision loss
decoder := json.NewDecoder(strings.NewReader(data))
decoder.UseNumber()
var result map[string]interface{}
decoder.Decode(&result)
n := result["age"].(json.Number)
age, _ := n.Int64() // 30, no float64 rounding
fmt.Println(age) // 30For partially dynamic responses — where the outer shape is known but an inner field varies — use json.RawMessage to defer parsing of that field.
// json.RawMessage defers parsing of the "data" field
type APIResponse struct {
Status string `json:"status"`
Data json.RawMessage `json:"data"` // raw bytes, parse later
}
var resp APIResponse
json.Unmarshal(payload, &resp)
// Now decide what to decode based on Status
if resp.Status == "user" {
var u User
json.Unmarshal(resp.Data, &u)
} else if resp.Status == "order" {
var o Order
json.Unmarshal(resp.Data, &o)
}Marshal: Go struct to JSON
Converting Go values back to JSON uses json.Marshal for compact output or json.MarshalIndent for human-readable pretty-printed output. Both return a []byte slice and an error.
package main
import (
"encoding/json"
"fmt"
"log"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age,omitempty"`
}
func main() {
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
// Compact JSON
b, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
// {"id":1,"name":"Alice","email":"alice@example.com"}
// Note: Age is omitted because it is zero and has omitempty
// Pretty-printed JSON
pretty, err := json.MarshalIndent(user, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(pretty))
// {
// "id": 1,
// "name": "Alice",
// "email": "alice@example.com"
// }
}The second and third arguments to json.MarshalIndent are the prefix and indent strings. An empty prefix with " " (two spaces) is the most common choice for readable output. Use "\t" for tab indentation.
To write JSON directly to an io.Writer (such as an HTTP response), use json.NewEncoder — this avoids the intermediate []byte allocation:
// Write JSON to an HTTP response
func handler(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
// Encode appends a trailing newline automatically
}Error handling
encoding/json returns typed errors that let you distinguish between malformed JSON and type mismatches. Use errors.As to inspect the specific error type and extract offset or field information for better diagnostics.
package main
import (
"encoding/json"
"errors"
"log"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func parseUser(data []byte) (User, error) {
var user User
err := json.Unmarshal(data, &user)
if err != nil {
var syntaxErr *json.SyntaxError
var unmarshalErr *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxErr):
log.Printf("JSON syntax error at byte offset %d", syntaxErr.Offset)
case errors.As(err, &unmarshalErr):
log.Printf(
"wrong type for field %q: expected %s, got %s",
unmarshalErr.Field,
unmarshalErr.Type,
unmarshalErr.Value,
)
default:
log.Printf("JSON parse error: %v", err)
}
return User{}, err
}
return user, nil
}
func main() {
// Syntax error: missing closing brace
parseUser([]byte(`{"id":1,"name":"Alice"`))
// Type error: id should be a number, not a string
parseUser([]byte(`{"id":"not-a-number","name":"Alice"}`))
}Common errors you will encounter:
| Error type | Cause | Key fields |
|---|---|---|
*json.SyntaxError | Malformed JSON (missing bracket, trailing comma, etc.) | Offset int64 |
*json.UnmarshalTypeError | JSON value cannot be stored in the Go type | Field string, Value string |
io.EOF | Empty input passed to Unmarshal or Decoder | — |
io.ErrUnexpectedEOF | Input was truncated mid-stream | — |
When using json.NewDecoder, you can also call decoder.DisallowUnknownFields() before Decode to return an error whenever the JSON contains a key with no matching struct field — useful for strict API validation.
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // strict mode
var user User
if err := decoder.Decode(&user); err != nil {
// Returns an error if JSON has keys not in User struct
http.Error(w, "unknown fields in request body", http.StatusBadRequest)
return
}Frequently asked questions
Does Go's encoding/json support JSON comments?
No, standard JSON doesn't allow comments and Go's parser rejects them. If you need comments in config files, use a preprocessor to strip them before parsing, or switch to TOML or YAML for configuration files where human annotations are important.
Why are JSON numbers float64 in interface?
The JSON specification doesn't distinguish integers from floating-point numbers — all numeric values share the same type. Go defaults to float64 because it is the largest numeric type that can represent all valid JSON numbers without data loss. Use json.Number (with decoder.UseNumber()) or decode into a typed struct field to work with integers directly.
What is the difference between json.Unmarshal and json.NewDecoder?
json.Unmarshal takes a []byte slice that is already fully loaded in memory. json.NewDecoder wraps an io.Reader and reads data on demand. For files and HTTP response bodies, NewDecoder is more memory-efficient because it never requires the entire payload in RAM at once. For short byte slices you already have in memory, Unmarshal is simpler and involves one fewer allocation.
How do I ignore unknown JSON fields in Go?
By default encoding/json silently ignores JSON keys that have no matching struct field — you don't need to do anything. If you want the opposite behavior — returning an error when unexpected keys appear — call decoder.DisallowUnknownFields() on a json.Decoder before calling Decode.
Can I parse JSON without defining a struct?
Yes. Unmarshal into map[string]interface for JSON objects, []interface for JSON arrays, or use json.RawMessage to capture the raw bytes of a field and parse them later. This is useful for proxies, dynamic configs, and any scenario where the schema is not known at compile time.
How do I parse a JSON field that is sometimes null?
Use a pointer type for the field. For example, Name *string instead of Name string. When the JSON value is null or the key is absent, Go sets the pointer to nil. When the key is present with a string value, Go allocates a string and sets the pointer to it. Check with if user.Name != nil before dereferencing.
type User struct {
ID int `json:"id"`
Name *string `json:"name"` // nil when JSON is null or key is missing
}
data := []byte(`{"id":1,"name":null}`)
var u User
json.Unmarshal(data, &u)
if u.Name == nil {
fmt.Println("name is null")
} else {
fmt.Println(*u.Name)
}Validate your JSON output
Paste the JSON produced by your Go code into Jsonic's validator to catch syntax errors before they reach production. The validator highlights exact error positions and formats your JSON for easy inspection.
Open JSON Validator