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:
{"foo":42}
But what if the struct is from an imported package?
If the type is declared in another package and you cannot modify it, you usually cannot directly change its JSON field names using tags on the original type.
Instead, common solutions are:
- create your own local struct for JSON output
- create a wrapper struct
- implement custom JSON marshaling in your own type
- convert the data into a
map[string]anywhen appropriate
This matters in real programming because API responses often need a specific JSON format that does not match your internal Go types exactly.
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.
If you cannot edit the imported type
Suppose an imported package defines:
package other
type T struct {
Foo int
}
You can create your own response struct:
package main
import (
"encoding/json"
"fmt"
)
type ExternalT struct {
Foo int
}
type TResponse struct {
Foo int `json:"foo"`
}
func main() {
src := ExternalT{Foo: 42}
resp := TResponse{
Foo: src.Foo,
}
out, err := json.Marshal(resp)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
Output:
{"foo":42}
This is the most common and safest approach when working with imported structs.
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.
3. Marshal to JSON
out, err := json.Marshal(value)
encoding/json inspects the struct fields.
- It sees exported field
Foo - It checks for a
jsontag - It finds
json:"foo" - It uses
fooas the JSON key
4. Print the result
fmt.Println(string(out))
The output is:
{"foo":42}
What if there were no tag?
If the struct were:
type T struct {
Foo int
}
then json.Marshal(T{Foo: 42}) would produce:
{"Foo":42}
because the default key name is the field name itself.
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 MarshalJSON
If you need special formatting logic, you can implement:
func (t MyType) MarshalJSON() ([]byte, error) {
type alias struct {
Foo int `json:"foo"`
}
return json.Marshal(alias{Foo: t.Foo})
}
This is useful when output needs transformation, conditional fields, or compatibility behavior.
5. Validation and shaping before marshaling
Real code often validates or filters data before returning JSON.
- omit empty fields with
omitempty - rename fields with tags
- avoid exposing internal fields accidentally
- keep transport models stable even if internal structs change
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.
Fix
Make the field exported, then use a tag:
type T struct {
Foo int `json:"foo"`
}
3. Forgetting that imported types cannot be modified
If a struct comes from another package and has no JSON tags, you usually cannot change its marshaled field names directly.
Fix
Create your own struct:
type TResponse struct {
Foo int `json:"foo"`
}
Then copy the data.
4. Exposing internal structs directly in APIs
This makes your JSON output tightly coupled to internal code.
Fix
Use dedicated request/response structs for stable API contracts.
5. Using map[string]any for everything
This works, but it loses type safety.
m := map[string]any{
"foo": 42,
}
Better approach
Prefer structs with tags when the shape is known ahead of time.
6. Writing incorrect tag syntax
Broken code:
type T struct {
Foo int `json:foo`
}
Fix
Tags must be quoted correctly:
type T struct {
Foo int `json:"foo"`
}
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 |
Struct tags vs custom marshaling
| Option | When to use |
|---|---|
| Struct tags | When you only need to rename fields or add simple options like omitempty |
MarshalJSON | When you need computed fields, custom formatting, conditional output, or control over an imported type through a wrapper |
Imported struct vs local DTO
| Choice | Advantage | Tradeoff |
|---|---|---|
| Marshal imported struct directly | Less code | You cannot easily control JSON shape |
| Convert to local DTO | Full control over field names and output | Requires a conversion step |
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":42}
Ignore a field
type T struct {
Foo int `json:"-"`
}
Omit empty values
type T struct {
Foo int `json:"foo,omitempty"`
}
When the type is imported
Use one of these:
- local response struct
- wrapper type
- custom
MarshalJSON map[string]anyfor dynamic cases
Remember
foo int `json:"foo"`
will not be marshaled because foo is unexported.
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
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
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.