Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to go struct engine, best practices for implementation, and data security standards.
Go's approach to JSON is deliberately low-level: the standard library's encoding/json package handles serialization via struct field tags, with no magic or hidden runtime. Converting JSON to Go structs is not just about naming fields — it's about understanding how struct tags control marshaling behavior, how Go's zero-value semantics interact with missing JSON fields, and when to reach for pointer types, custom unmarshalers, or code-generated serializers for hot paths.
// Input JSON
{
"user_id": 101,
"user_name": "gopher_dev",
"email": "gopher@example.com",
"is_admin": false,
"profile": {
"bio": "Building things in Go",
"avatar_url": "https://cdn.example.com/avatars/101.jpg"
},
"tags": ["backend", "go"],
"last_login": "2024-01-15T08:30:00Z"
}
// Generated Go Struct
package models
import "time"
type Profile struct {
Bio string `json:"bio"`
AvatarURL string `json:"avatar_url"`
}
type User struct {
ID int `json:"user_id"`
Username string `json:"user_name"`
Email string `json:"email"`
IsAdmin bool `json:"is_admin"`
Profile Profile `json:"profile"`
Tags []string `json:"tags"`
LastLogin time.Time `json:"last_login"`
}
Go's encoding/json parses ISO 8601 strings into time.Time automatically. The struct tag `json:"user_id"` maps the JSON key user_id to the Go field ID — the naming convention mismatch between snake_case JSON and PascalCase Go is handled entirely in the tag.
Go has no null type — it uses zero values instead. Understanding when to use pointers vs. zero values vs. custom types is critical for correct JSON mapping:
type Order struct {
ID int `json:"id"`
Status string `json:"status"`
// Nullable — pointer indicates intentional null vs. absent
// *string = nil if JSON "discount_code" is null
// *string = &"SAVE10" if present
DiscountCode *string `json:"discount_code"`
// Optional — omit from JSON output if empty
InternalNote string `json:"internal_note,omitempty"`
// Nullable + omit if null in output
DeletedAt *time.Time `json:"deleted_at,omitempty"`
// Ignore this field entirely in JSON
rawPayload string `json:"-"`
}
// Checking a nullable field
if order.DiscountCode != nil {
fmt.Println("Discount:", *order.DiscountCode)
}
The ,omitempty tag omits a field from JSON output when it's the zero value ("", 0, false, nil). Pointers are essential when you need to distinguish between "field was present but null" and "field was absent" — both are the zero value without a pointer.
When the default mapping isn't sufficient — different date formats, custom type conversion, validation during parsing — implement json.Unmarshaler:
type UnixTimestamp time.Time
func (t *UnixTimestamp) UnmarshalJSON(data []byte) error {
var seconds int64
if err := json.Unmarshal(data, &seconds); err != nil {
return err
}
*t = UnixTimestamp(time.Unix(seconds, 0).UTC())
return nil
}
func (t UnixTimestamp) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(t).Unix())
}
type Event struct {
ID int `json:"id"`
Name string `json:"name"`
OccuredAt UnixTimestamp `json:"occurred_at"` // unix seconds in JSON
}
// Also useful: json.RawMessage for deferred parsing
type Webhook struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // parse later based on Type
}
func (w *Webhook) ParsePayload() (interface{}, error) {
switch w.Type {
case "payment":
var p PaymentPayload
return &p, json.Unmarshal(w.Payload, &p)
case "refund":
var r RefundPayload
return &r, json.Unmarshal(w.Payload, &r)
}
return nil, fmt.Errorf("unknown webhook type: %s", w.Type)
}
By default, json.Unmarshal silently ignores unknown fields — a source of subtle bugs when the API adds a new field you haven't handled. Use json.Decoder with DisallowUnknownFields() for strict validation:
func DecodeUser(r io.Reader) (*User, error) {
var u User
decoder := json.NewDecoder(r)
decoder.DisallowUnknownFields() // error on unknown JSON keys
if err := decoder.Decode(&u); err != nil {
return nil, fmt.Errorf("decoding user: %w", err)
}
return &u, nil
}
// For HTTP handlers in net/http
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// req is populated and validated structurally
}
For hot paths where reflection overhead matters — high-throughput APIs, real-time data processing — code-generate the marshalers:
# Install easyjson
go install github.com/mailru/easyjson/...@latest
# Generate marshal/unmarshal code for all structs in a file
easyjson -all models/user.go
# This creates models/user_easyjson.go with hand-optimized
# MarshalJSON / UnmarshalJSON methods — no reflection at runtime
easyjson generates code that is 2-5× faster than encoding/json by eliminating reflection entirely. The generated file is checked into your repo and regenerated after struct changes. Use it for structs that are serialized on every request in production.
// Standard library handler
func GetUser(w http.ResponseWriter, r *http.Request) {
user := User{ ID: 1, Username: "gopher_dev" }
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// Gin — automatic binding and JSON response
type CreateUserReq struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
}
func (h *Handler) CreateUser(c *gin.Context) {
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// req.Username, req.Email — validated
user, err := h.db.CreateUser(c, req.Username, req.Email)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"})
return
}
c.JSON(http.StatusCreated, user)
}
*string distinguishes between absent and empty string. Zero-value ambiguity is a common Go JSON bug — a missing count field and a count: 0 field both produce 0 without pointers.encoding/json only validates structural types. Use go-playground/validator with struct tags (validate:"required,email") for business rule validation, or validate explicitly after unmarshaling.DisallowUnknownFields() for user-facing APIs to catch mismatches early. Be lenient for third-party webhooks where the payload might grow over time.encoding/json grows slices dynamically. For large JSON arrays, pre-allocate with make([]Item, 0, expectedLen) and append via streaming decoder for memory efficiency.Q: How do I handle JSON keys that are Go reserved words?
A: Use a struct tag to rename: Type string `json:"type"`. The JSON key type maps to the Go field named however you choose — EventType, Kind, or even Type (Go allows exported fields named after types).
Q: How do I unmarshal JSON arrays at the top level?
A: Unmarshal into a slice: var users []User; json.Unmarshal(data, &users). For streaming large arrays (too large to fit in memory), use json.Decoder and call decoder.Token() to advance past the opening bracket, then loop calling decoder.Decode(&item).
Q: What is the difference between json.Marshal and json.Encoder?
A: json.Marshal returns a byte slice — use it when you need the JSON as bytes in memory. json.NewEncoder(w).Encode(v) writes directly to an io.Writer — more efficient for HTTP responses or file writes because it avoids the intermediate byte slice allocation.
Q: How do I handle dynamic JSON with unknown structure?
A: Use map[string]interface{} for fully dynamic JSON, or json.RawMessage to defer parsing of a specific field. For partially-known structures, define the known fields as struct fields and add a map[string]json.RawMessage field with the tag `json:"-"` and implement a custom UnmarshalJSON that captures the remainder.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.