Skip to content

JSON to Go Struct

Generate Go struct definitions from a JSON sample. Emits json tags and a struct per nested object.

This tool generates Go struct definitions from a sample JSON value in your browser; nothing leaves your machine. The generated code is a starting point — it captures the shape of the JSON, not the rules behind it.

The basic mapping

JSON:
{
  "name": "Alice",
  "age": 30,
  "active": true
}

Generated Go:
type Root struct {
    Name   string `json:"name"`
    Age    int    `json:"age"`
    Active bool   `json:"active"`
}

Three things happen:

  1. Field name. PascalCase Go identifier derived from the JSON key. name → Name. Required because Go only marshals/unmarshals exported (capitalized) fields.
  2. Type. Inferred from the JSON value. String → string, integer-looking number → int, decimal-looking number → float64, boolean → bool, null → *T (pointer to the inferred type), object → nested struct, array → slice.
  3. Struct tag. json:"name" tells encoding/json the original JSON key. Without it, Go marshals the Go field name (Name), not the JSON key (name).

Number type inference

The trickiest mapping. JSON has a single numeric type; Go has many.

JSONInferred Go
42int
42.0float64 (the trailing .0 signals fractional)
42.5float64
1e10float64 (exponential)
9999999999 (large)int64 or float64 (depending on tool)

The trap: if the sample has 42 but the production data sometimes has 42.5, the generated int field will fail to unmarshal the decimal. Generators based on a single sample can’t know the full range.

Defensive choice: use float64 whenever the field is “number-typed” rather than “definitely-integer.” Tightens to int only when you’re sure.

Optional fields and pointers

JSON’s null is awkward in Go’s static type system. Two conventions:

// Pointer to type — distinguishes null/absent from zero value
type User struct {
    Email *string `json:"email"`
}

// Type with omitempty — drops empty values when marshaling
type User struct {
    Email string `json:"email,omitempty"`
}

The pointer form distinguishes “key is null” from “key is missing” from “key is empty string.” The omitempty form treats empty/zero as equivalent to absent.

For round-trip-safe deserialization, prefer the pointer form. For producing JSON, omitempty is more idiomatic Go.

The tool above can’t infer a type from null, so a null field becomes interface{}; replace it with the pointer form (*string etc.) once you know the real type. Keys that aren’t valid Go names are converted (user_id → UserId, 2fa → X2fa) and the original key stays in the json: tag.

Nested objects → nested structs

JSON:
{
  "user": {
    "name": "Alice",
    "address": {
      "city": "NYC"
    }
  }
}

Generated Go:
type Root struct {
    User User `json:"user"`
}

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

type Address struct {
    City string `json:"city"`
}

Each nested object becomes its own type. Some generators inline anonymous structs:

type Root struct {
    User struct {
        Name    string `json:"name"`
        Address struct {
            City string `json:"city"`
        } `json:"address"`
    } `json:"user"`
}

Anonymous structs are compact for one-off use; named structs are easier to test, mock, and reuse.

Arrays

JSON: [1, 2, 3]
Go:   []int

JSON: [{"name": "Alice"}]
Go:   []struct { Name string `json:"name"` }

For an array of objects, the inferred type is []<element type>. The element type is inferred from the first array element (or the union of elements, in better tools).

What generation can’t infer

  • Required vs optional. If the sample has name, is it always present, or did this row just happen to include it? The tool can’t know without multiple samples.
  • Validation rules. email is a string; the tool can’t infer that it should match an email regex.
  • Enums. A field whose value is one of "draft", "published", "archived" is best modeled as a Go enum, not as a string. The tool emits string.
  • Numeric ranges. A field that always holds 0–100 is generated as int with no constraint. The tool can’t tell.
  • Polymorphism. A data field that holds different shapes depending on a type field needs custom unmarshaling logic. Generators emit a struct with all fields nullable, which is a workable but lossy approximation.

These all require human review of the generated types. Treat the output as a draft.

Struct tags beyond json:

Modern Go projects often have multiple tag families:

type User struct {
    ID    int    `json:"id"    db:"user_id" validate:"required"`
    Email string `json:"email" db:"email"   validate:"required,email"`
}

db: for SQL ORM mapping (sqlx, similar). validate: for input validation libraries. The tool above generates only json: tags; add others manually if your project uses them.

Common pitfalls

  • Generating from one sample. Capture variations: with and without optional fields, with null and without, with empty arrays, with the maximum-realistic field counts. Single-sample generation is brittle.
  • Treating large numbers as int. int in Go is 32 or 64 bits depending on platform. For known 64-bit-needed values, use int64 explicitly.
  • Re-running generation over hand-modified code. Lose hand-tuning. Generation is one-shot; treat the output as scaffold and check it into source control before customizing.
  • Forgetting omitempty when producing JSON. Without it, empty strings, zero numbers, and false booleans serialize as "", 0, and false rather than being omitted. Sometimes that’s correct; usually consumers want truly-optional fields omitted.
  • Renaming JSON keys. When the upstream JSON’s keys are snake_case, the generated tags say json:"some_key". If you change the field name in Go, the tag doesn’t update — the JSON-side key is still some_key. Both have to be edited together.

For other JSON-to-language conversions, see JSON to TypeScript, JSON to Python dataclass.