Go (Golang) has become one of the most popular languages for building APIs, microservices, and cloud-native applications. At the heart of nearly every Go web service is the encoding/json package, which provides the ability to marshal (serialize) Go structs into JSON and unmarshal (deserialize) JSON data into Go structs. This bidirectional conversion is fundamental to how Go applications communicate with the outside world.
When you receive a JSON response from an API, you need a Go struct that mirrors the JSON structure. Each field in the struct must have the correct type, and each field needs a json struct tag that maps it to the corresponding JSON key. For simple JSON payloads with a few fields, writing these structs by hand is straightforward. But when you are dealing with deeply nested objects, arrays of mixed types, or API responses with dozens of fields, manual struct creation becomes tedious and error-prone.
This guide covers everything you need to know about converting JSON to Go structs: the type mapping rules, how struct tags work, handling nested objects and arrays, dealing with optional and nullable fields, custom unmarshaling, and best practices for production code. Whether you are a Go beginner or an experienced developer, understanding these concepts will make you more productive when working with JSON in Go.
JSON has six data types: string, number, boolean, null, object, and array. Go has a much richer type system, so there are multiple valid Go types for some JSON types. Understanding the mapping rules is essential for writing correct and efficient structs.
JSON strings map directly to Go string. This is the simplest and most common mapping. JSON string values can contain Unicode characters, escape sequences, and any valid UTF-8 text, all of which Go handles natively since Go strings are UTF-8 encoded by default.
// JSON: {"name": "John Doe"}
type User struct {
Name string `json:"name"`
}
JSON does not distinguish between integers and floating-point numbers -- all numbers are represented the same way. In Go, however, you must choose the appropriate numeric type. The general rules are:
42, -7, 0) should typically map to int. Use int64 for values that may exceed 32-bit range, such as database IDs, timestamps in milliseconds, or file sizes in bytes.3.14, -0.5) should map to float64. Go's encoding/json package uses float64 as the default numeric type when unmarshaling into interface{}.float64 to avoid floating-point rounding errors.// JSON: {"age": 30, "balance": 1250.75, "id": 9007199254740993}
type Account struct {
Age int `json:"age"`
Balance float64 `json:"balance"`
ID int64 `json:"id"`
}
JSON booleans (true and false) map to Go bool. This is a direct one-to-one mapping with no ambiguity.
// JSON: {"is_active": true}
type User struct {
IsActive bool `json:"is_active"`
}
JSON null is one of the trickier values to handle in Go. A non-pointer Go field cannot represent null -- it will always have a zero value. To distinguish between "the field was null" and "the field had a zero value," use pointer types or the special interface{} type.
// JSON: {"middle_name": null}
// Option 1: Pointer type (recommended)
type User struct {
MiddleName *string `json:"middle_name"`
}
// Option 2: interface{} (less type-safe)
type User struct {
MiddleName interface{} `json:"middle_name"`
}
JSON objects map to Go structs. Each key in the JSON object becomes a field in the struct. For nested objects, you can either define a separate named struct or use an anonymous inline struct (more on this in the nested structs section).
JSON arrays map to Go slices. The element type of the slice depends on the array contents: []string for arrays of strings, []int for arrays of integers, []MyStruct for arrays of objects, and []interface{} for mixed-type or empty arrays.
// JSON: {"tags": ["go", "json"], "scores": [95, 87, 92]}
type Data struct {
Tags []string `json:"tags"`
Scores []int `json:"scores"`
}
Go struct tags are string annotations attached to struct fields that provide metadata used by packages like encoding/json. The json tag is the most commonly used tag and controls how the field is marshaled to and unmarshaled from JSON.
The basic json tag specifies the JSON key name that corresponds to the struct field. Without a json tag, the encoding/json package uses the field name as-is (case-sensitive). Since JSON conventionally uses snake_case or camelCase while Go uses PascalCase, tags are essential for maintaining idiomatic naming in both languages.
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
}
The omitempty option tells the JSON encoder to skip the field if it has a zero value. Zero values in Go are: "" for strings, 0 for numbers, false for booleans, nil for pointers, interfaces, slices, and maps. This is useful for optional fields in API requests.
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone,omitempty"` // Optional
Website string `json:"website,omitempty"` // Optional
}
With omitempty, if Phone is an empty string, the marshaled JSON will not include the "phone" key at all. Without omitempty, it would appear as "phone": "".
Using json:"-" tells the encoder to completely ignore the field during both marshaling and unmarshaling. This is useful for internal fields that should not be exposed in JSON.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Password string `json:"-"` // Never include in JSON output
}
The string option tells the encoder to marshal and unmarshal the value as a JSON string, even if the Go type is numeric or boolean. This is useful when an API sends numbers as quoted strings.
// JSON: {"count": "42"}
type Data struct {
Count int `json:"count,string"`
}
Real-world JSON data is rarely flat. API responses typically contain nested objects, sometimes several levels deep. Go provides two approaches for handling nested JSON objects: separate named structs and inline anonymous structs.
The most common approach is to define each nested object as its own named struct type. This promotes reusability, testability, and clarity.
type Address struct {
Street string `json:"street"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip_code"`
}
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Address Address `json:"address"`
}
Separate structs are preferred when the nested object represents a distinct domain concept (like Address, Coordinates, or Metadata), when the same nested structure appears in multiple parent structs, or when you need to write methods on the nested type.
For one-off nested structures that are only used in a single place, Go allows inline anonymous struct definitions directly within the parent struct.
type User struct {
Name string `json:"name"`
Address struct {
Street string `json:"street"`
City string `json:"city"`
} `json:"address"`
}
Inline structs reduce the number of type definitions in your package but sacrifice reusability and make it harder to write helper functions or methods for the nested data.
Go's struct embedding (anonymous fields) can be combined with JSON to create compositions. When a struct is embedded without a field name, its fields are promoted to the parent level during JSON marshaling and unmarshaling.
type Timestamps struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Timestamps // Embedded: fields are promoted
}
// JSON: {"id": 1, "name": "John", "created_at": "2026-01-01", "updated_at": "2026-03-15"}
JSON arrays are ubiquitous in API responses. Paginated lists, tag collections, nested item arrays -- understanding how Go handles these is critical for building robust API clients and servers.
Arrays of simple values (strings, numbers, booleans) map directly to typed Go slices.
type Project struct {
Tags []string `json:"tags"`
Versions []float64 `json:"versions"`
Flags []bool `json:"flags"`
}
Arrays of objects are one of the most common patterns in APIs. Each object in the array is represented by a struct, and the array becomes a slice of that struct type.
type Comment struct {
ID int `json:"id"`
Author string `json:"author"`
Body string `json:"body"`
}
type Post struct {
Title string `json:"title"`
Comments []Comment `json:"comments"`
}
While uncommon in well-designed APIs, some JSON responses contain arrays with mixed types (e.g., [1, "hello", true]). In Go, these must be represented as []interface{} since Go slices are homogeneous.
type Data struct {
MixedValues []interface{} `json:"mixed_values"`
}
Arrays of arrays (matrices, grids) are represented as slices of slices in Go.
// JSON: {"matrix": [[1,2,3], [4,5,6]]}
type Data struct {
Matrix [][]int `json:"matrix"`
}
One of the most common challenges when working with JSON in Go is handling optional or nullable fields. JSON has a clear distinction between a missing field, a field set to null, and a field with a zero value. Go's type system does not have this distinction by default, which is where pointer types become essential.
Consider a PATCH endpoint where the client sends only the fields they want to update. If the client sends {"name": "John"}, how do you know whether age was intentionally omitted (don't update) or should be set to 0? With a non-pointer int field, both cases result in the zero value 0 after unmarshaling.
Pointer types solve this by adding a third state: nil (field was missing or null), zero value (field was explicitly set to 0), and non-zero value. After unmarshaling, you can check if the pointer is nil to determine if the field was present.
type UpdateUserRequest struct {
Name *string `json:"name,omitempty"`
Age *int `json:"age,omitempty"`
Email *string `json:"email,omitempty"`
}
func handleUpdate(req UpdateUserRequest) {
if req.Name != nil {
// Name was provided, update it
user.Name = *req.Name
}
if req.Age != nil {
// Age was provided (could be 0), update it
user.Age = *req.Age
}
// Email was not provided, leave unchanged
}
Use pointer types when you need to distinguish between "field absent" and "field is zero value," which is common in PATCH/update endpoints, optional configuration fields, and API responses where null has semantic meaning. For required fields that always have a value, non-pointer types are simpler and more efficient.
Sometimes the default JSON unmarshaling behavior does not match the data format you are working with. Go allows you to implement the json.Unmarshaler interface to customize how a type is deserialized from JSON.
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Any type that implements this interface will have its UnmarshalJSON method called instead of the default behavior when the JSON decoder encounters a value for that type.
APIs often return dates in formats other than RFC 3339 (which Go's time.Time expects). A custom unmarshaler lets you handle this.
type CustomDate struct {
time.Time
}
func (d *CustomDate) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
t, err := time.Parse("2006-01-02", s)
if err != nil {
return err
}
d.Time = t
return nil
}
type Event struct {
Name string `json:"name"`
Date CustomDate `json:"date"`
}
// JSON: {"name": "Launch", "date": "2026-03-27"}
Some APIs inconsistently return a field as either a string or a number. A custom unmarshaler can handle both cases gracefully.
type FlexibleInt struct {
Value int
}
func (f *FlexibleInt) UnmarshalJSON(b []byte) error {
var n int
if err := json.Unmarshal(b, &n); err == nil {
f.Value = n
return nil
}
var s string
if err := json.Unmarshal(b, &s); err == nil {
n, err := strconv.Atoi(s)
if err != nil {
return err
}
f.Value = n
return nil
}
return fmt.Errorf("cannot unmarshal %s into int", string(b))
}
Many APIs wrap their response data in a standard envelope containing metadata like pagination, status codes, or error messages. Go generics (introduced in Go 1.18) make this pattern cleaner.
type APIResponse[T any] struct {
Data T `json:"data"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
TotalCount int `json:"total_count,omitempty"`
}
// Usage:
var resp APIResponse[[]User]
json.Unmarshal(body, &resp)
for _, user := range resp.Data {
fmt.Println(user.Name)
}
When JSON keys are dynamic (not known at compile time), use map[string]interface{} or typed maps like map[string]string.
type Config struct {
Name string `json:"name"`
Settings map[string]interface{} `json:"settings"`
Labels map[string]string `json:"labels"`
}
When you need to delay parsing of a JSON field until you know its type, use json.RawMessage. This is common in event systems where different event types have different payload structures.
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"`
}
func processEvent(data []byte) error {
var event Event
json.Unmarshal(data, &event)
switch event.Type {
case "user_created":
var user User
json.Unmarshal(event.Payload, &user)
// Handle user creation
case "order_placed":
var order Order
json.Unmarshal(event.Payload, &order)
// Handle order
}
return nil
}
Go convention dictates that common acronyms should be fully capitalized: ID not Id, URL not Url, HTTP not Http. This applies to struct fields that will be exposed in public APIs.
type Resource struct {
ID int `json:"id"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
APIKey string `json:"api_key"`
HTTPCode int `json:"http_code"`
}
Even if the JSON key matches the Go field name, add explicit json tags. This makes the mapping explicit, protects against breaking changes if you rename the Go field, and serves as documentation for other developers reading your code.
Don't use the same struct for both API request bodies and response bodies. Requests often have fewer fields, different validation rules, and different omitempty requirements than responses. Define separate types like CreateUserRequest and UserResponse.
Go's encoding/json package does not validate required fields, string lengths, value ranges, or business rules. Always validate your data after unmarshaling, either with custom validation functions or a validation library like go-playground/validator.
When reading JSON from an HTTP response body or file, use json.NewDecoder(reader).Decode(&v) instead of reading the entire body into memory first. This is more memory-efficient for large payloads.
func handleRequest(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Process user...
}
By default, encoding/json silently ignores unknown JSON fields. If you want to enforce strict parsing (reject unknown fields), use json.Decoder with DisallowUnknownFields().
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&user); err != nil {
// Will error on unknown fields
}
Our free JSON to Go Struct Converter automates the conversion process described in this guide. Simply paste your JSON into the left panel and get instantly generated Go struct definitions in the right panel.
The tool supports all the features discussed in this article: proper type inference (distinguishing int from float64), nested struct generation (both separate and inline), omitempty tags, Go naming conventions (PascalCase with proper acronym handling), and arrays of objects. It handles edge cases like null values, empty arrays, and deeply nested structures.
All processing happens entirely in your browser -- no data is sent to any server. You can use the tool offline once the page has loaded. Try it now with your API response data to save time writing boilerplate struct definitions.
Paste your JSON and get Go struct definitions instantly. Free, no sign-up required.
Open JSON to Go Converter →