Question
I want to use Go's encoding/json package to marshal a struct that is declared in an imported package.
For example:
type T struct {
Foo int
}
Because the struct comes from another package, its exported fields begin with uppercase letters. If I marshal it like this:
out, err := json.Marshal(&T{Foo: 42})
the result is:
{"Foo":42}
However, I want the JSON output to use lowercase key names instead:
{"foo":42}
Is there an easy way to do this when using encoding/json in Go?
Short Answer
By the end of this page, you will understand how Go's encoding/json package chooses JSON field names, why exported struct fields appear with uppercase names by default, and how to produce lowercase keys using struct tags, wrapper types, or custom marshaling patterns.
Concept
Go's encoding/json package converts struct fields into JSON object keys. By default, it uses the exact exported field name as the JSON key.
That means this struct:
type T struct {
Foo int
}
becomes:
{"Foo":42}
Why this happens
In Go, only exported fields are visible to packages like encoding/json. Exported names start with an uppercase letter.
So there are two separate rules at work:
- Go visibility rule: fields must be exported to be accessible to
encoding/json - JSON naming rule: if no tag is provided, the field name itself becomes the JSON key
The normal solution: struct tags
If you control the struct definition, the standard solution is to add a JSON tag:
type T struct {
Foo int `json:"foo"`
}
Now the JSON output becomes:
Mental Model
Think of a Go struct as a form with labeled fields.
- The Go field name is the internal label, like
Foo - The JSON tag is a custom external label, like
foo
Without a tag, encoding/json prints the label exactly as written.
So if the field says Foo, the JSON key will be Foo.
A JSON tag is like putting a sticker over the original label so the outside world sees foo instead.
If the form belongs to another package and you cannot place your own sticker on it, you make a copy of the data into a new form that has the labels you want.
Syntax and Examples
The basic syntax for controlling JSON key names in Go is a struct tag.
Struct tag syntax
type T struct {
Foo int `json:"foo"`
}
Example: lowercase JSON key
package main
import (
"encoding/json"
"fmt"
)
type T struct {
Foo int `json:"foo"`
}
func main() {
out, err := json.Marshal(T{Foo: 42})
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Output:
{"foo":42}
What changed?
The field is still named Foo in Go, so it remains exported and usable by encoding/json.
But the JSON tag tells the encoder to use foo as the output key.
Step by Step Execution
Consider this example:
package main
import (
"encoding/json"
"fmt"
)
type T struct {
Foo int `json:"foo"`
}
func main() {
value := T{Foo: 42}
out, err := json.Marshal(value)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Step-by-step
1. Define the struct
type T struct {
Foo int `json:"foo"`
}
- The Go field name is
Foo - It is exported because it starts with an uppercase letter
- The JSON tag says to encode it as
foo
2. Create a value
value := T{Foo: 42}
A struct value is created with Foo set to 42.
Real World Use Cases
Lowercase JSON keys are common in real applications because many APIs follow JSON naming conventions such as camelCase or snake_case.
Common scenarios
- REST API responses
- Your Go struct might use
UserID, but your API should returnuserId
- Your Go struct might use
- Third-party API integration
- An external service may require exact field names like
access_token
- An external service may require exact field names like
- Frontend compatibility
- JavaScript clients often expect lowercase or camelCase keys
- Data transformation layers
- Internal domain models may not match public JSON contracts
- Backward compatibility
- You may need to preserve an existing JSON schema even if Go field names change
Example: API response model
type User struct {
ID int
Name string
}
type UserResponse struct {
ID int `json:"id"`
Name string `json:"name"`
}
Even if your internal model and response model contain the same data, keeping a separate response struct gives you precise control over the API shape.
Real Codebase Usage
In real projects, developers rarely expose imported structs directly as JSON if the output format matters.
Common patterns
1. Separate API/DTO structs
A DTO (Data Transfer Object) is a struct designed specifically for input or output.
type Product struct {
SKU string
Price int
}
type ProductResponse struct {
SKU string `json:"sku"`
Price int `json:"price"`
}
This pattern is clean and explicit.
2. Mapping from domain models to response models
func ToProductResponse(p Product) ProductResponse {
return ProductResponse{
SKU: p.SKU,
Price: p.Price,
}
}
This is common in handlers and service layers.
3. Wrapper structs around imported types
If you need only part of an imported value:
type PublicT struct {
Foo int `json:"foo"`
}
Then copy the values you want to expose.
4. Custom
Common Mistakes
1. Assuming encoding/json automatically lowercases field names
Broken expectation:
type T struct {
Foo int
}
Many beginners expect this to become:
{"foo":42}
But it actually becomes:
{"Foo":42}
Fix
Use a struct tag:
type T struct {
Foo int `json:"foo"`
}
2. Trying to use an unexported field
Broken code:
type T struct {
foo int `json:"foo"`
}
This field is unexported, so encoding/json will ignore it.
Comparisons
Here is how the common approaches compare.
| Approach | Works with imported type? | Type-safe | Best for | Notes |
|---|---|---|---|---|
| Struct tag on original type | No, unless you control the type | Yes | Your own structs | Standard and simplest solution |
| Separate response struct | Yes | Yes | APIs and clean architecture | Most common real-world pattern |
Wrapper type with custom MarshalJSON | Yes | Yes | Special formatting logic | More flexible, more code |
map[string]any | Yes | No | Quick dynamic output | Easy, but less maintainable |
Cheat Sheet
Quick rules
encoding/jsononly marshals exported struct fields- Exported Go fields start with an uppercase letter
- By default, JSON keys use the Go field name exactly
- To rename a key, use a struct tag
- If the type is from another package and cannot be edited, create your own output struct or wrapper
Basic syntax
type T struct {
Foo int `json:"foo"`
}
Default behavior
type T struct {
Foo int
}
Output:
{"Foo":42}
Renamed field
type T struct {
Foo int `json:"foo"`
}
Output:
{"foo":
FAQ
Can Go automatically lowercase struct field names in JSON?
No. By default, encoding/json uses the exported field name exactly as written unless you add a JSON tag.
How do I make a JSON key lowercase in Go?
Use a struct tag like this:
Foo int `json:"foo"`
Can I change JSON tags on a struct from another package?
No. You cannot add tags to a type you do not control. Instead, create your own struct or wrapper type.
Why doesn't encoding/json use unexported fields?
Because unexported fields are not accessible outside their package through reflection in the way encoding/json needs.
What is the best way to control JSON output for imported structs?
Usually, create a separate response/DTO struct with the exact JSON tags you want and copy the needed fields.
Should I use a map instead of a struct for custom JSON keys?
Only if the JSON shape is dynamic. If the structure is known, structs are safer and easier to maintain.
When should I implement MarshalJSON?
Use it when tags are not enough, such as for computed values, conditional fields, or wrapping imported types with custom output rules.
Mini Project
Description
Build a small JSON response adapter for a type that simulates coming from another package. This project demonstrates the real-world pattern of converting an internal or external struct into a dedicated API response struct with lowercase JSON keys.
Goal
Create a Go program that converts a source struct into a JSON-friendly response struct with lowercase field names.
Requirements
- Define a source struct with exported fields such as
FooandBar. - Define a separate response struct that uses JSON tags like
json:"foo"andjson:"bar". - Copy data from the source struct into the response struct.
- Marshal the response struct to JSON and print it.
- Ensure the output keys are lowercase.
Keep learning
Related questions
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.