Last month I was building a Go service that needed to consume a Stripe-like payment webhook. The webhook payload had about 40 fields spread across 6 nested objects. I typed out the first struct by hand, hit go build, and got 8 type errors. Wrong types on amount (should be int64, not float64), missed a json:"-" tag on an internal field, and completely skipped two nested structs because I didn't scroll down far enough.

I spent 20 minutes debugging types I should never have written in the first place. That's when I decided: no more manual Go structs from JSON.

Why Go Makes You Care About Types

Go's type system is strict. Unlike dynamic languages where you can just json.Unmarshal into a map[string]interface{} and call it a day, Go wants you to define every field, every tag, every nested struct. And honestly? That's a good thing. But it means the boilerplate is real.

Take a typical API response:

{
  "id": "evt_1ABC123",
  "object": "event",
  "api_version": "2023-10-01",
  "created": 1696543200,
  "data": {
    "object": {
      "id": "pi_3XYZ789",
      "amount": 2999,
      "currency": "usd",
      "status": "succeeded",
      "metadata": {
        "order_id": "ORD-001",
        "customer_tier": "premium"
      }
    }
  },
  "pending_webhooks": 2
}

If I were writing this by hand, I'd need to define at least 3 structs with proper json tags. And I'd probably get the tags wrong — maybe use "meta_data" instead of "metadata", breaking the unmarshaling silently.

With a JSON-to-Go converter, I paste the JSON and get this:

type WebhookEvent struct {
    ID              string        `json:"id"`
    Object          string        `json:"object"`
    APIVersion      string        `json:"api_version"`
    Created         int64         `json:"created"`
    Data           WebhookData   `json:"data"`
    PendingWebhooks int           `json:"pending_webhooks"`
}

type WebhookData struct {
    Object PaymentIntent `json:"object"`
}

type PaymentIntent struct {
    ID       string            `json:"id"`
    Amount   int64             `json:"amount"`
    Currency string            `json:"currency"`
    Status   string            `json:"status"`
    Metadata map[string]string `json:"metadata"`
}

Notice the json:"api_version" tag maps to APIVersion in Go. The converter handles snake_case to PascalCase automatically, which is the Go convention. And amount correctly becomes int64 instead of float64 because the JSON has no decimal point.

For more on working with complex data transformations, our guide on /blog/jsonformatter--convert-json-to-csv-without-code covers flattening nested structures for analysis.

Struct Tags: The Devil Is in the Details

Struct tags in Go do more than just name mapping. You've got json:, xml:, yaml:, bson:, gorm:, and a dozen more. Each serves a different purpose.

Here's what a good converter should give you:

type Product struct {
    ID          string   `json:"id"`
    Name        string   `json:"name"`
    Description string   `json:"description,omitempty"`
    Price       float64  `json:"price"`
    Tags        []string `json:"tags,omitempty"`
    CreatedAt   string   `json:"created_at"`
    UpdatedAt   string   `json:"updated_at"`
    InternalID  string   `json:"-"`
}
  • omitempty on optional fields — if the field is empty, it won't be serialized
  • - for internal fields you don't want in JSON at all
  • string tag option for fields that should be marshaled as strings

When I generate structs from JSON, I can focus on adding the right tag options instead of typing out every field name. If you're also working with YAML configurations, our guide on /blog/jsonformatter--json-to-yaml-conversion-guide shows how the same data looks across formats.

Nested Structures and Embedded Types

Real JSON isn't flat. APIs love nesting. A good converter should generate separate struct types for each level of nesting, then compose them.

{
  "user": {
    "address": {
      "street": "123 Main St",
      "city": "San Francisco",
      "zip": "94105"
    },
    "company": {
      "name": "Acme Corp",
      "role": "Engineer"
    }
  }
}

This should produce:

type User struct {
    Address Address `json:"address"`
    Company Company `json:"company"`
}

type Address struct {
    Street string `json:"street"`
    City   string `json:"city"`
    Zip    string `json:"zip"`
}

type Company struct {
    Name string `json:"name"`
    Role string `json:"role"`
}

No flat struct with UserAddressStreet string. No hand-typed nesting. Just clean, composable types.

Edge Cases I've Hit (and How Converters Handle Them)

Hyphenated keys: JSON keys like "shipping-address" — valid JSON, but not valid Go identifiers. Converters rename them, usually to ShippingAddress.

Empty arrays vs. typed arrays: An empty "items": [] can't tell the converter what type the array holds. You'll need to fill in the type manually after generation.

OneOf patterns: Polymorphic JSON where a field can be different types. Converters can't auto-detect this, but you can generate separate structs for each variant and use json.RawMessage to handle them.

Null fields: A "deleted_at": null field generates *string (a pointer type). This is actually great — it forces you to handle the nil case in your code.

If you need to handle these converted structs in Java or Python too, take a look at /blog/jsonformatter--json-to-java-pojo-python-dataclass for the cross-language approach.

My Workflow for JSON → Go

Here's what I actually do:

  1. Get the JSON from API docs, curl, or Postman
  2. Open DevFormatters JSON formatter
  3. Paste JSON and switch to Go output
  4. Copy the generated structs into a types.go file
  5. Add json.RawMessage or custom UnmarshalJSON methods where needed
  6. Write test data using the generated structs

The whole process takes about 2 minutes for a complex payload. Doing it by hand? 15-20 minutes, with guaranteed debugging time.

FAQ

Q: How do I convert JSON to a Go struct for free?

A: Use an online tool like DevFormatters — paste JSON, select Go output, and copy the generated structs.

Q: Does the converter generate json: struct tags automatically?

A: Yes. It maps each field to its JSON key and adds the appropriate tag. Snake_case keys map to PascalCase field names.

Q: What about omitempty and other tag options?

A: Most converters add omitempty for nullable or optional fields. You can customize tags yourself after generation.

Q: How does it handle deeply nested JSON?

A: The converter creates a separate Go struct for each nested level, keeping your type definitions clean and composable.

Q: What if my JSON has fields with special characters?

A: Special characters like hyphens or spaces are stripped or converted. For example, "shipping-address" becomes ShippingAddress.

Q: Does it handle time.Time fields?

A: Most converters treat date-like strings as string. You'll need to manually change them to time.Time and add the appropriate parsing logic.

Q: Can I generate structs for MongoDB using bson tags instead?

A: Some converters offer tag customization. If not, you can do a find-and-replace from json: to bson:.

Q: What if the JSON array is empty — how does the converter know the type?

A: It can't. Empty arrays will need you to manually specify the element type after generation.