Question

Why Go Struct to JSON Returns Empty Object {}: Exported Fields and json.Marshal

gojson

Question

I am trying to convert a Go struct to JSON using Go's encoding/json package, but the result is always an empty object: {}.

Here is the code:

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    name string
}

func main() {
    user := &User{name: "Frank"}
    b, err := json.Marshal(user)
    if err != nil {
        fmt.Printf("Error: %s", err)
        return
    }
    fmt.Println(string(b))
}

When I run it, the output is:

{}

Why does json.Marshal produce {} instead of including the name field, and how do I fix it?

Short Answer

By the end of this page, you will understand why json.Marshal ignores some struct fields in Go, how exported vs unexported fields work, and how to correctly produce JSON using exported field names and optional struct tags.

Concept

In Go, the encoding/json package only reads exported struct fields when converting a struct to JSON.

A field is exported if its name starts with an uppercase letter:

type User struct {
    Name string
}

A field is unexported if it starts with a lowercase letter:

type User struct {
    name string
}

In your original code, the field is name, which is unexported. Because of that, json.Marshal cannot include it in the JSON output, so the result is:

{}

This rule exists because Go uses capitalization to control visibility across packages. The encoding/json package is a separate package, so it can only access exported fields.

To fix the problem, rename the field to Name:

type User struct {
    Name string
}

Now json.Marshal can access it and will output:

{"Name":"Frank"}

If you want the JSON key to be lowercase, use a struct tag:

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

That produces:

{"name":"Frank"}

This concept matters a lot in real Go programs because JSON is commonly used for:

  • APIs
  • configuration files
  • request and response bodies
  • storing structured data
  • logging and messaging systems

If your fields are not exported correctly, your JSON may silently miss data.

Mental Model

Think of a Go struct as a building with many rooms.

  • Exported fields are rooms with open doors and labels on them, so other packages can enter and read them.
  • Unexported fields are rooms with locked doors, visible only inside the same package.

The encoding/json package is like a visitor from another building. It can only enter rooms with open doors.

So if your struct looks like this:

type User struct {
    name string
}

json.Marshal walks up to the name field, finds the door locked, and skips it. With no readable fields left, it outputs:

{}

If you change it to:

type User struct {
    Name string
}

now the door is open, and json.Marshal can include it in the JSON.

Take Quiz

Syntax and Examples

Basic fix

Make the field exported by starting it with an uppercase letter.

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name string
}

func main() {
    user := &User{Name: "Frank"}
    b, err := json.Marshal(user)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(string(b))
}

Output:

{"Name":"Frank"}

Using a JSON tag for lowercase keys

If you want the JSON field name to be name instead of Name, add a tag:

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

Example:

package main

import (
    "encoding/json"
    "fmt"
)

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

func main() {
    user := User{Name: "Frank"}
    b, _ := json.Marshal(user)
    fmt.Println(string(b))
}

Output:

{"name":"Frank"}

Multiple fields example

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

When marshaled:

user := User{
    Name:  "Frank",
    Email: "frank@example.com",
    Age:   30,
}

Output:

{"name":"Frank","email":"frank@example.com","age":30}

Example with an unexported field still ignored

type User struct {
    Name     string `json:"name"`
    password string `json:"password"`
}

Even though password has a JSON tag, it is still unexported, so it will not appear in the output.

That means this:

user := User{Name: "Frank", password: "secret"}
b, _ := json.Marshal(user)
fmt.Println(string(b))

prints:

{"name":"Frank"}

The tag does not override Go's export rules.

Step by Step Execution

Consider this working example:

package main

import (
    "encoding/json"
    "fmt"
)

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

func main() {
    user := User{Name: "Frank"}
    b, err := json.Marshal(user)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(b))
}

Step-by-step trace

1. Define the struct

type User struct {
    Name string `json:"name"`
}
  • The struct has one field: Name
  • It starts with uppercase N, so it is exported
  • The JSON tag says: use name as the key in JSON

2. Create a value

user := User{Name: "Frank"}

Now the user value contains:

  • Name = "Frank"

3. Marshal the struct

b, err := json.Marshal(user)

The encoding/json package:

  • inspects the struct
  • finds exported fields
  • sees Name
  • reads the tag json:"name"
  • creates JSON with key name
  • stores the result as bytes in b

b now contains bytes representing:

{"name":"Frank"}

4. Check for errors

if err != nil {
    fmt.Println(err)
    return
}
  • If marshaling fails, print the error
  • In this case, there is no error

5. Print the JSON

fmt.Println(string(b))
  • b is a byte slice
  • converting it with string(b) makes it readable text
  • output becomes:
{"name":"Frank"}

What happens in the broken version?

If the field is written like this:

type User struct {
    name string
}

then during marshaling:

  • encoding/json inspects the struct
  • it sees name
  • name is unexported
  • it skips the field
  • no fields are included
  • result becomes:
{}

Real World Use Cases

This concept appears constantly in Go development.

Building JSON APIs

When returning data from an HTTP handler, structs must use exported fields:

type Response struct {
    Message string `json:"message"`
    Success bool   `json:"success"`
}

If fields are lowercase, clients may receive empty objects or missing data.

Parsing and generating request/response bodies

Go services often send and receive JSON between microservices. Exported fields ensure the payload contains the expected values.

Configuration files

Applications may load or save JSON config files using structs. If fields are unexported, config values may not be written correctly.

Database and API models

Developers often define structs that are reused for:

  • database records
  • API responses
  • background jobs
  • event payloads

Correct field exporting and JSON tags keep data consistent across systems.

Logging structured data

Some systems serialize structs into JSON logs. Exported fields determine what appears in the log output.

Take Quiz

Real Codebase Usage

In real projects, developers usually combine exported fields with JSON tags.

Common pattern: exported field + lowercase JSON key

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

This keeps Go code idiomatic while matching common JSON naming conventions.

Validation before marshaling

Developers often validate struct values before returning them:

func buildUserResponse(name string) ([]byte, error) {
    if name == "" {
        return nil, fmt.Errorf("name cannot be empty")
    }

    user := User{Name: name}
    return json.Marshal(user)
}

This uses a guard clause to fail early.

Hiding internal fields

Real codebases often keep internal data out of JSON intentionally:

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

Here password stays internal because it is unexported.

DTO-style structs

Teams often create dedicated structs for API responses instead of exposing full internal models directly.

type UserResponse struct {
    Name string `json:"name"`
}

This avoids accidentally exposing fields.

Error handling pattern

A standard Go pattern is:

b, err := json.Marshal(data)
if err != nil {
    return err
}

Even though missing exported fields often do not cause an error, invalid values in some cases can.

Common Mistakes

1. Using lowercase field names

Broken code:

type User struct {
    name string
}

Problem:

  • name is unexported
  • json.Marshal ignores it

Fix:

type User struct {
    Name string
}

2. Thinking JSON tags make unexported fields work

Broken code:

type User struct {
    name string `json:"name"`
}

Problem:

  • The tag does not make the field exported
  • The field is still ignored

Fix:

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

3. Forgetting to convert bytes to string when printing

Code:

b, _ := json.Marshal(user)
fmt.Println(b)

This prints a byte slice, not readable JSON.

Better:

fmt.Println(string(b))

4. Expecting pointers to change export behavior

Code like this still fails if fields are unexported:

user := &User{name: "Frank"}

Problem:

  • Passing a pointer does not bypass export rules

5. Mixing Go naming and JSON naming

Beginners sometimes write this:

type User struct {
    name string
    user_email string
}

That is not idiomatic Go for exported JSON fields.

Better:

type User struct {
    Name      string `json:"name"`
    UserEmail string `json:"user_email"`
}

6. Ignoring omitempty behavior later on

When learners add tags like this:

Name string `json:"name,omitempty"`

the field is omitted when empty. That can make it look like marshaling is broken when it is actually working as designed.

Comparisons

Exported vs unexported fields in Go

Field nameExported?Accessible from other packages?Included by json.Marshal?
NameYesYesYes
nameNoNoNo

Without tag vs with tag

Struct fieldJSON output
Name string{"Name":"Frank"}
Name string \json:"name"``{"name":"Frank"}

Pointer vs value when marshaling

InputWorks?Notes
json.Marshal(user) where user is a struct valueYesNormal use
json.Marshal(&user) where user is a structYesAlso normal

Pointers and values both work for marshaling. The important issue here is field export visibility, not whether the struct is passed by pointer.

Export rule vs JSON tag

FeatureWhat it doesWhat it does not do
Exported field (Name)Makes field visible to other packagesDoes not change JSON key name by itself
JSON tag (json:"name")Changes JSON key nameDoes not make an unexported field visible
Take Quiz

Cheat Sheet

Quick rule

json.Marshal only includes exported struct fields.

Exported field

type User struct {
    Name string
}

Unexported field

type User struct {
    name string
}

This becomes:

{}

Lowercase JSON key with tag

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

Output:

{"name":"Frank"}

Core syntax

b, err := json.Marshal(value)
if err != nil {
    // handle error
}
fmt.Println(string(b))

Important rules

  • Uppercase field name = exported = JSON can use it
  • Lowercase field name = unexported = JSON ignores it
  • JSON tags rename fields in output
  • Tags do not override export rules
  • Struct value and pointer both work with json.Marshal

Common fix

From:

type User struct {
    name string
}

To:

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

FAQ

Why does Go json.Marshal return {} for my struct?

Because your struct fields are unexported, usually due to starting with lowercase letters. encoding/json only marshals exported fields.

Do I need to use uppercase field names in Go structs for JSON?

Yes, if you want encoding/json to access those fields. Use uppercase Go field names and JSON tags if you want lowercase JSON keys.

Can I use a JSON tag on a lowercase field in Go?

You can write the tag, but it will not help. Unexported fields are still ignored.

How do I make the JSON output use name instead of Name?

Use a tag:

Name string `json:"name"`

Does json.Marshal work differently for pointers and struct values?

No. Both pointers and values work. The issue is field visibility, not pointer usage.

Why doesn't Go return an error when fields are skipped?

Skipping unexported fields is expected behavior, so encoding/json simply ignores them instead of treating them as an error.

How can I hide a field from JSON on purpose?

Use an unexported field or use a tag like:

Password string `json:"-"`

That tells the JSON encoder not to include it.

Take Quiz

Mini Project

Description

Create a small Go program that converts user data into JSON for an API-style response. This demonstrates the exact rule behind the original problem: exported struct fields are included in JSON, and JSON tags control the final key names.

Goal

Build a program that marshals a User struct into readable JSON with lowercase JSON field names.

Requirements

  1. Define a User struct with at least three exported fields.
  2. Use JSON struct tags so the output keys are lowercase.
  3. Create a user value with sample data.
  4. Convert the struct to JSON using json.Marshal.
  5. Print the JSON output as a string.
Take Quiz

Keep learning

Related questions

View more