Fix 'json: cannot unmarshal string into Go value of type int' Error in Go

intermediate๐Ÿ”ท Go2026-03-23| Go 1.16+, any OS (Linux, macOS, Windows), common when consuming REST APIs or reading JSON config files

Error Message

json: cannot unmarshal string into Go value of type int
#json#unmarshal#struct#encoding#api

The Error

You're decoding JSON into a Go struct and suddenly hit something like this:

json: cannot unmarshal string into Go value of type int
json: cannot unmarshal number into Go value of type string
json: cannot unmarshal object into Go value of type []string

This blows up inside json.Unmarshal or json.NewDecoder(...).Decode(...). One mismatched field poisons the whole operation โ€” nothing gets decoded.

Why This Happens

Go's encoding/json package doesn't do type coercion. If your struct expects an int but the JSON delivers "42" โ€” a quoted string โ€” the decoder refuses to convert it. Full stop. The same happens in reverse: a bare JSON number won't go quietly into a string field.

Three situations trigger this the most:

  • A third-party API returning numbers as strings (e.g., "id": "123" instead of "id": 123) โ€” this is shockingly common with payment APIs and older REST services
  • A legacy database or system that serializes every column as a string
  • Frontend form data where JavaScript coerces numbers into quoted values before sending

Step-by-Step Fix

Step 1 โ€” Identify the mismatched field

The error tells you the Go type it expected. Match that against your struct to find the field. Here's a concrete example:

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Age  int    `json:"age"`
}

// Incoming JSON from API:
// {"id": "99", "name": "Alice", "age": 30}
// ^ "id" is a string in JSON but int in struct โ†’ triggers the error

Step 2 โ€” Option A: Fix the JSON source (preferred)

Got control over the API or data source? Fix it there. Numbers belong unquoted in JSON:

// Wrong
{"id": "99", "age": "30"}

// Correct
{"id": 99, "age": 30}

Clean at the root. No workarounds needed.

Step 3 โ€” Option B: Use a string field + convert manually

Can't touch the source? Change the struct field to string, unmarshal cleanly, then convert it yourself:

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

type User struct {
    ID   string `json:"id"`   // accept string from JSON
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)

    var u User
    if err := json.Unmarshal(data, &u); err != nil {
        panic(err)
    }

    // Convert after unmarshal
    id, err := strconv.Atoi(u.ID)
    if err != nil {
        panic(fmt.Sprintf("invalid id: %v", err))
    }
    fmt.Println(id) // 99
}

Step 4 โ€” Option C: Use json.Number for flexible numeric fields

json.Number is a string type under the hood that holds any JSON number. You pull the actual value out explicitly:

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID   json.Number `json:"id"`
    Name string      `json:"name"`
}

func main() {
    data := []byte(`{"id": 99, "name": "Alice"}`)

    var u User
    json.Unmarshal(data, &u)

    id, _ := u.ID.Int64()
    fmt.Println(id) // 99
}

Step 5 โ€” Option D: Implement a custom UnmarshalJSON

Some APIs are just inconsistent โ€” "id" comes back as 99 in one response and "99" in another. A custom unmarshaler handles both:

import (
    "encoding/json"
    "strconv"
)

type FlexInt int

func (f *FlexInt) UnmarshalJSON(b []byte) error {
    // Try as number first
    var n int
    if err := json.Unmarshal(b, &n); err == nil {
        *f = FlexInt(n)
        return nil
    }
    // Fall back to string
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    n, err := strconv.Atoi(s)
    if err != nil {
        return err
    }
    *f = FlexInt(n)
    return nil
}

type User struct {
    ID   FlexInt `json:"id"`
    Name string  `json:"name"`
}

func main() {
    // Works with both {"id": 99} and {"id": "99"}
    data := []byte(`{"id": "99", "name": "Alice"}`)
    var u User
    if err := json.Unmarshal(data, &u); err != nil {
        panic(err)
    }
    // u.ID is 99
}

Verifying the Fix

Simplest check โ€” inspect err after decoding:

var u User
if err := json.Unmarshal(data, &u); err != nil {
    fmt.Println("still broken:", err)
} else {
    fmt.Printf("decoded: %+v\n", u)
}

Better yet, write a unit test. It catches regressions automatically:

func TestDecodeUser(t *testing.T) {
    data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)
    var u User
    if err := json.Unmarshal(data, &u); err != nil {
        t.Fatal(err)
    }
    if int(u.ID) != 99 {
        t.Fatalf("expected 99, got %d", u.ID)
    }
}

Tips

Inspect the JSON payload before writing your struct

Raw API responses are messy. Before you define a single Go struct, format the JSON and look at it. I use ToolCraft's JSON Formatter โ€” paste in the raw response and you can immediately see which numbers are quoted and which aren't. Everything runs in the browser; nothing gets uploaded, which matters when the payload contains user data.

Nested fields can hide the real culprit

The mismatch isn't always at the top level. A field three levels deep can trigger the same error. Print the error's type to get more detail:

var u User
if err := json.Unmarshal(data, &u); err != nil {
    fmt.Printf("type: %T, detail: %v\n", err, err)
}

Struct tags are case-sensitive

Your json:"fieldname" tag must match the JSON key exactly โ€” casing included. Go struct fields are exported (uppercase) by convention, but REST APIs almost always send lowercase keys. A tag like json:"UserID" won't match "userid" in the payload.

Don't use interface{} as an escape hatch

Decoding into map[string]interface{} sidesteps this error. But it's not free. Numeric values silently become float64, you lose type safety entirely, and every field access downstream requires a type assertion. Typed structs are more verbose upfront and far less painful later.

Related Error Notes