LEARN · DEBUGGING GUIDE

Debugging Go JSON Unmarshal Unexpected Type Errors

Unexpected type errors in Go JSON unmarshaling usually mean your struct fields don't match the JSON shape. This guide shows you exactly where to look and how to fix it fast.

IntermediateGo5 min read

What this usually means

The JSON data you're trying to parse has a different structure than your Go struct expects. This often happens when you use interface{} as a catch-all type and then try to cast the result to a specific type without checking. The Go JSON decoder automatically maps JSON numbers to float64 when decoding into interface{}, and JSON objects to map[string]interface{}. If your code then assumes a different concrete type, you get an interface conversion panic. Other common causes: missing or misspelled struct tags, nested JSON arrays vs. objects, or using the wrong type for a field (e.g., string vs. number).

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Print the raw JSON payload to a file: curl -v https://api.example.com/data 2>&1 | tee raw.json
  • 2Run a minimal Go test: json.Unmarshal(rawJSON, &yourStruct) and log the error with %+v
  • 3Add debug output using json.Decoder with DisallowUnknownFields() to catch extra fields
  • 4Use a JSON validator (jq . raw.json) to confirm the JSON structure matches your expectations
  • 5Check struct tags: ensure json:"fieldname" matches the JSON keys exactly (case-sensitive)
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe Go source file defining the struct (look at json:"" tags)
  • searchThe JSON payload at runtime (log it with fmt.Printf("JSON: %s\n", rawJSON))
  • searchThe error message from json.Unmarshal — it tells you the exact field path
  • searchAny interface{} usage in your struct — that's where type assertions fail
  • searchYour go.sum and go.mod for the json package version (stdlib is fine, but some use gjson)
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningStruct field type mismatch: JSON has a number, Go field is string (or vice versa)
  • warningUsing interface{} and then type-asserting to the wrong Go type
  • warningJSON has a nested object where Go expects a flat string (or missing nested struct)
  • warningJSON keys use camelCase but Go struct tags are PascalCase or missing
  • warningJSON array where Go expects an object (or missing [] in struct field declaration)
  • warningUnmarshal into a nil pointer: the target must be a non-nil pointer to a struct
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildChange the Go struct field type to match the JSON: string → float64, int, or custom type
  • buildUse json.RawMessage for fields with dynamic structure, then parse conditionally
  • buildImplement json.Unmarshaler interface on custom types to handle type coercion
  • buildAdd DisallowUnknownFields() to detect extra fields early
  • buildUse UseNumber() on decoder to avoid float64 for integers: decoder.UseNumber()
  • buildFor nested objects, define matching inner structs and use proper tags
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedWrite a table-driven test with example JSON and expected struct values
  • verifiedRun json.Unmarshal with the exact payload and assert no error
  • verifiedPrint the struct after unmarshal with %+v to confirm all fields populated
  • verifiedUse -race flag to catch concurrent map writes if you're decoding into shared maps
  • verifiedTest with edge cases: null values, empty arrays, missing fields
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningBlindly using interface{} everywhere — it defers type checks to runtime
  • warningIgnoring the error from json.Unmarshal (always check err != nil)
  • warningType-asserting from interface{} without a comma-ok pattern (val, ok := x.(T))
  • warningAssuming JSON numbers are ints — they decode as float64 into interface{}
  • warningForgetting to export struct fields (Go requires capital first letter)
  • warningUsing json.Unmarshal on a []byte from a non-UTF-8 source
( 07 )War story

The Payment API That Panicked on a Number

Backend EngineerGo 1.18, Gin, PostgreSQL

Timeline

  1. 09:15Deploy new payment endpoint that parses webhook JSON from Stripe
  2. 09:42First alert: panic in production - interface conversion: interface {} is float64, not string
  3. 09:45Check logs: panic at payment.go:48, type assertion on amount_captured
  4. 09:50Dump raw JSON from webhook: amount_captured is 1099 (number, not string)
  5. 09:55Inspect struct: AmountCaptured string json:"amount_captured" - mismatch!
  6. 10:00Hotfix: change field type to float64, redeploy
  7. 10:05Confirm no more panics, all subsequent webhooks processed
  8. 10:15Write regression test with exact Stripe payload

I had just deployed a new webhook handler for Stripe payments. The first sign of trouble was a PagerDuty alert: panic in production. I opened the logs and saw a familiar but dreaded line: interface conversion: interface {} is float64, not string. The stack trace pointed to my type assertion on amount_captured.

I quickly dumped the raw JSON from the webhook request body using a temporary log line. Stripe was sending amount_captured as a number (1099) but my struct had it as a string. I had copied the struct from an older API that used string amounts, but Stripe's API docs clearly show it's an integer. My bad.

I hotfixed by changing the field type to float64 and added UseNumber() to the decoder to avoid float64 precision issues. Then I wrote a test using the actual Stripe test payload. The lesson: always validate the real JSON payload against your struct, and never blindly type-assert from interface{} without checking the concrete type first.

Root cause

Struct field declared as string but JSON contained a number, causing a panic on type assertion.

The fix

Changed the field type to float64 and added json.Decoder UseNumber() for numeric precision.

The lesson

Always inspect the raw JSON payload and match your struct types exactly; use UseNumber() when dealing with monetary values.

( 08 )Why interface{} Causes Unexpected Type Errors

When you unmarshal JSON into an interface{}, the Go json package uses default types: JSON numbers become float64, strings become string, booleans become bool, null becomes nil, objects become map[string]interface{}, and arrays become []interface{}. This is documented but often forgotten.

If you then type-assert the result to a different type (e.g., int), you get a panic. The fix is to either define a concrete struct, or use a type switch to handle each possible type. For numbers, consider using json.Number via decoder.UseNumber() to avoid float64 conversion.

( 09 )Using json.RawMessage for Dynamic Fields

If part of your JSON has a variable structure, don't use interface{}. Instead, use json.RawMessage for that field. RawMessage preserves the raw JSON bytes, allowing you to defer unmarshaling to a concrete type based on some discriminator field.

Example: type Event struct { Type string; Data json.RawMessage }. Then after unmarshal, you can switch on Event.Type and unmarshal Data into the appropriate struct. This avoids interface{} entirely.

( 10 )Detecting Struct-JSON Mismatches Early

Use json.Decoder with DisallowUnknownFields() to catch JSON keys that don't match any struct field. This prevents silent data loss when the API adds new fields.

Also, set decoder.UseNumber() to avoid float64 for integers. Then you can access numbers via json.Number which implements String() and Int64() methods, reducing type assertion panics.

( 11 )Common Pitfall: Unmarshal into Nil Pointer

json.Unmarshal requires a non-nil pointer. If you pass a nil pointer, you get an error like 'json: Unmarshal(nil *main.MyStruct)'. Always initialize your variable: var s MyStruct; err := json.Unmarshal(data, &s). For slices/maps, use make or var with no nil.

Frequently asked questions

Why does json.Unmarshal turn my integer into a float64?

When unmarshaling into an interface{}, Go's json package uses float64 for all JSON numbers. To preserve the exact number representation, use json.Decoder with UseNumber() and then access via json.Number.

How do I debug 'json: cannot unmarshal object into Go value of type string'?

This means the JSON has an object (curly braces) where your Go struct expects a string field. Check the JSON key path in the error message and ensure your struct field type matches: either change the struct field to a nested struct or use json.RawMessage.

Can I unmarshal JSON with missing fields without errors?

Yes, by default json.Unmarshal ignores missing fields. To enforce strict matching, use json.Decoder with DisallowUnknownFields() which returns an error if the JSON contains unknown fields.

What's the difference between json.Unmarshal and json.Decoder?

json.Unmarshal works on a []byte, while json.Decoder works on an io.Reader stream. Use Decoder when you want fine-grained control (e.g., UseNumber, DisallowUnknownFields). Both produce the same result for complete payloads.

Why does my struct field remain empty after unmarshal?

Common reasons: the JSON key doesn't match the struct tag (case-sensitive), the struct field is unexported (lowercase), or you unmarshaled into a nil pointer. Check the exact JSON key and ensure the field is exported.