DEV Community

Rasika Dangamuwa
Rasika Dangamuwa

Posted on

JSON to Go Structs: 5 Edge Cases That Break Type Safety in Golang

Go's standard encoding/json package requires predefined struct types to deserialize JSON data safely. While Go's strong typing prevents runtime surprises once data is parsed, converting arbitrary or nested JSON objects into idiomatic Go structs is full of subtle traps.

When consuming third-party APIs or legacy endpoints, naive struct generation frequently introduces subtle bugs—from silent zero-value overwrites to numeric precision loss. Here are five common edge cases in JSON-to-Go struct mapping and how to handle them cleanly.


1. null, Missing Fields, and Zero-Value Ambiguity

In JSON, there is a distinct difference between a key having a value of null, a key being entirely omitted, and a key having a zero value (like 0, false, or "").

Consider this JSON response:

{
  "user_id": 1042,
  "bio": null,
  "is_active": false
}
Enter fullscreen mode Exit fullscreen mode

If mapped to a standard Go struct:

type UserResponse struct {
    UserID   int    `json:"user_id"`
    Bio      string `json:"bio"`
    IsActive bool   `json:"is_active"`
}
Enter fullscreen mode Exit fullscreen mode

Go will unmarshal "bio": null into an empty string "". Your application logic can no longer distinguish between a user who set their bio to empty versus a user whose bio is null (or unconfigured).

To preserve null semantics:

  • Use pointer types (e.g., Bio *string), where null unmarshals to nil.
  • Or use sql.NullString / custom unmarshalers if pointer allocations are a performance concern.

2. Large Integers and float64 Precision Loss

By default, when Go unmarshals JSON numbers into an untyped interface{} or any, it parses all numbers as float64. This creates major issues with 64-bit integer IDs (like Twitter Snowflakes or 64-bit database primary keys):

{
  "transaction_id": 9223372036854775807
}
Enter fullscreen mode Exit fullscreen mode

If unmarshaled into float64, IEEE 754 floating-point representation loses precision beyond $2^{53} - 1$ ($9,007,199,254,740,991$), silently corrupting the ID.

Solution: Always explicitly declare int64 or uint64 in your struct tags rather than relying on generic map types:

type FinancialRecord struct {
    TransactionID int64 `json:"transaction_id,string"` // if passed as string
}
Enter fullscreen mode Exit fullscreen mode

If the upstream API returns large numbers directly as unquoted JSON integers, set Decoder.UseNumber() when setting up your JSON decoder.


3. Mixed-Type Arrays and Polymorphic JSON

REST APIs sometimes return arrays containing heterogeneous data types or polymorphic objects:

{
  "events": [
    { "type": "click", "x": 120, "y": 450 },
    { "type": "input", "text": "hello world" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Attempting to model events as []Event with a single struct will force optional fields with omitempty across all payload variants. When working with complex API payloads, using an in-browser utility like Nutilz JSON to Go allows you to instantly generate struct definitions with proper struct tags and nested type inferencing without transmitting sensitive payload data to external servers.

For polymorphic arrays in Go:

  • Unmarshal into []json.RawMessage first.
  • Inspect the "type" discriminator field.
  • Unmarshal each item into its specific concrete struct (ClickEvent vs InputEvent).

4. Struct Tag Case Conventions and Acronym Collisions

Go field names must be exported (capitalized) to be visible to encoding/json. Automatic converters typically convert snake_case JSON keys to PascalCase Go field names.

However, idiomatic Go prefers initialisms to remain uppercase (e.g., URL, HTTP, ID, UUID, IP):

// Anti-pattern
type Config struct {
    ApiUrl string `json:"api_url"`
}

// Idiomatic Go
type Config struct {
    APIURL string `json:"api_url"`
}
Enter fullscreen mode Exit fullscreen mode

Inconsistent naming can lead to confusion across teams and break linter rules like golangci-lint.


5. Overusing omitempty on Boolean and Numeric Fields

The omitempty struct tag instructs Go to skip serializing fields equal to their zero value. But for booleans and numbers, false and 0 are valid values:

type AccountStatus struct {
    IsDisabled bool `json:"is_disabled,omitempty"`
}
Enter fullscreen mode Exit fullscreen mode

If IsDisabled is false, json.Marshal will omit is_disabled entirely from the JSON payload instead of outputting "is_disabled": false. In Go 1.24+, the new omitzero tag helps resolve zero-value vs empty-value ambiguities for structs implementing isZero().


Conclusion

Translating complex JSON structures into clean Go code requires careful attention to pointers, numeric precision, and field tags. Automated tooling can save significant developer time when bootstrapping these definitions.

Whenever you are integrating a third-party API in Go, test your struct definitions against representative sample payloads. For fast client-side generation without backend data logging, try out Nutilz's JSON to Go converter.

Top comments (0)