JSON → Go struct.
Paste a JSON sample, get idiomatic Go types — nested structs broken out, common initialisms (ID URL HTTP) capitalised, ISO timestamps detected as time.Time, json tags preserving the original case. The same logic mholt/json-to-go popularised, in your browser.
type Root struct {
ID int64 `json:"id"`
Name string `json:"name"`
Active bool `json:"active"`
Born int64 `json:"born"`
Skills []string `json:"skills"`
Address Address `json:"address"`
Tags interface{} `json:"tags"`
}
type Address struct {
Street string `json:"street"`
City string `json:"city"`
}MixedCaps and capital initialisms.
Go's identifier conventions are unusually opinionated for a mainstream language, and nowhere does that opinion bite harder than in code generated from external data. The canonical rule, codified in Effective Go and enforced by every linter worth running, is MixedCaps for exported names, no shows, and initialisms preserved in their original case. That last clause is what catches teams off guard: it is UserID, not UserId. It is HTTPClient, not HttpClient. It is ServeHTTP, ParseURL, JWTToken, XMLDecoder.
The list of recognised initialisms is not folklore; it lives in the lint rules themselves, originally in golint's lintNames table, inherited by golangci-lint through the revive and stylecheck analyzers. ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, JWT, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, GID, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, and XSS — these are the names a generator must know.
The reason this matters more for code generation than for hand-written code is volume. A human writing a struct will reach for UserID instinctively after a week on a Go team. A naive generator that takes user_id from JSON and produces UserId ships hundreds of fields at a time, every one of them a future lint warning or a future rename PR. For roughly a decade — between the early popularity of mholt/json-to-go and the gradual hardening of in-tree initialism handling — Go developers pasted JSON into a converter, copied the output into their editor, and then went field-by-field correcting Id to ID and Url to URL.
The escape hatch that makes any of this workable is the struct tag. Go's encoding/json decouples the Go-side field name from the wire-side key entirely: a field declared as UserID int64 `json:"user_id"` is correct Go on the left and correct JSON on the right, and the encoder does the translation at runtime via reflection. This is why a generator can — and should — be aggressive about idiomatic Go names while preserving exact JSON keys in tags. The tag is the contract with the outside world; the field name is the contract with the rest of your codebase.
One sample reveals limited truth.
Type inference from a single JSON document is a constrained problem with a few well-known traps. The encoding/json package, when asked to unmarshal a number into an interface, always produces a float64 — there is no integer in the JSON data model, and the standard decoder does not try to guess. A generator targeting strongly-typed structs has more freedom: if every observed value of a field is a whole number within int64 range, emitting int64 is safe and ergonomic, and the decoder will happily parse 42 into it. If any observed value has a fractional part, the field has to be float64. If the sample shows an ISO-8601 timestamp string, an ISO-8601 detector can promote it to time.Time and add the time import, since the standard (*time.Time).UnmarshalJSON accepts RFC 3339.
Nullability and absence are where one sample fails silently. A null in the sample tells you the field can be null, but the absence of null does not tell you the field is non-null. The only sound default for an observed null is either interface (loses type information) or *T (preserves it). An empty array is even thinner evidence — element type is unknown, so generators emit []interface{} and hope the user replaces it. None of these inferences are wrong; they are simply under-determined.
| Sample value | Reasonable Go type | Caveat |
|---|---|---|
| 42 | int64 | could be float64 if other samples disagree |
| 42.5 | float64 | precision limited at 2^53 |
| "2026-04-26T10:30:00Z" | time.Time | requires time import |
| null | interface or *T | absence vs presence indistinguishable |
| [] | []interface | element type unknown |
| {} | named struct or map | shape unknown |
Whether a field is nullable in the source schema, whether it is optional (sometimes absent from the document), and whether a field that appears as a scalar today might appear as an array tomorrow under some less-common code path. These have to come from a schema, a corpus of samples, or human judgement.
Three states, two slots.
Go's zero-value semantics collide with JSON's three-state world (present-with-value, present-with-null, absent) in a way that a single struct field cannot fully express. A field declared Name string will deserialize the JSON {"name": ""} and the JSON {} into the same Go value: the empty string. If the distinction matters — and for PATCH endpoints, audit logs, or any system where "user explicitly cleared this field" is meaningful — you need a pointer. *string deserializes empty object to nil and the present-empty-string to a non-nil pointer to the empty string. The cost is a heap allocation per field and the verbosity of dereferencing at every read site.
The omitempty tag option compounds the confusion. On Marshal, omitempty causes the encoder to skip the field when its value is the zero value: empty string, zero int, nil pointer, nil slice, nil map. This is exactly what you want when emitting partial updates or compact responses. On Unmarshal it does nothing — the option only affects encoding. The dangerous interaction is using omitempty on a string field: marshaling cannot distinguish "user cleared the value" from "user never set the value", and round-tripping a struct through your API silently drops legitimate empty strings.
| Field shape | Distinguishes absent vs zero? | Heap alloc | Typical use |
|---|---|---|---|
| T | no | no | required scalar fields |
| T,omitempty | no (loses zero on output) | no | response DTOs, never PATCH input |
| *T | yes | yes | nullable columns, PATCH bodies |
| *T,omitempty | yes both directions | yes | full three-state JSON merge |
This is why ent, sqlc, and the various OpenAPI generators emit pointers for nullable database columns by default. They do not know what the consumer will do with the value, but they know that collapsing the three-state column into a two-state Go field is information loss the user cannot recover. A JSON-to-struct generator with no schema input has to take a more conservative line — emit non-pointer types by default to keep generated code readable, and let the user opt into pointers for fields they know are nullable.
JSON's float-only number trap.
The JSON specification, RFC 8259, defines Number as a decimal literal with no upper bound on magnitude or precision. In practice, almost every JSON implementation parses numbers into IEEE-754 double-precision floats, and the JavaScript ecosystem cemented this convention because that is what Number is in the language. The consequence: any integer larger than 2^53 (9,007,199,254,740,992) loses precision on round-trip through a generic JSON parser. This is not theoretical. Twitter's tweet IDs crossed 2^53 in 2009 and the API has emitted them as both integer and string since, with the explicit recommendation to use the string form. Snowflake IDs, Discord snowflakes, and most distributed-ID schemes share the same property.
Go offers three reasonable defenses. The first is json.Number, a string-typed wrapper returned by the decoder when configured with decoder.UseNumber(). It defers conversion: you call .Int64(), .Float64(), or .String() when you need a typed value, and you never lose the original text. The second is json.RawMessage, a byte-slice alias that captures the raw JSON of a field without parsing it. The third — most common in practice for APIs that emit large IDs — is to type the field as string on the Go side and document that the wire format is also a string.
The default json.Unmarshal into interface will route every number through float64. If you decode a JSON document containing 9007199254740993 and re-marshal, you get 9007199254740992 back. There is no warning, no error, no loss visible to the program. The only way to detect this is to know it can happen and design around it.
Faster libraries, familiar tradeoffs.
The standard encoding/json package is correct, well-documented, and slower than most production workloads would prefer. Its reflection-based approach pays a per-field cost on every call, and for high-throughput services that cost is measurable. The Go ecosystem has produced several alternatives, each making a different tradeoff. json-iterator/go (jsoniter) offers a drop-in replacement with the same API and roughly 2-3× the throughput, achieved through smarter reflection caching and a hand-written parser. mailru/easyjson takes the codegen path: you run easyjson against your struct definitions and it emits hand-rolled MarshalJSON/UnmarshalJSON methods, eliminating reflection entirely for 5-10× speedups on encode-heavy paths. ByteDance's sonic uses JIT compilation and SIMD-aware parsing to push further still on amd64.
The decision is rarely about peak throughput in isolation. The standard library is the lingua franca: every Go developer knows it, every linter understands it, and every edge case (NaN, Inf, time formats, custom marshalers) behaves the way the documentation says. Codegen libraries drift. easyjson's generated code reflects the encoding/json semantics of the version it was built against, and when stdlib changes — say, the Go 1.18 fix for JSON encoding of certain floats, or the Go 1.21 changes to time.Time.MarshalJSON — the generated code does not. This is fine if you regenerate on every Go upgrade and pin the easyjson version in your go.mod, less fine if generated files have been hand-edited or sitting in the tree for two years.
For most services, stdlib is enough; reach for an alternative when profiling shows JSON serialization in the top three CPU consumers, and pin the version when you do.
One source of truth, many generated artefacts.
A JSON-to-struct generator solves a tactical problem: you have a sample, you want a struct, you want it now. The strategic question is which artifact is the source of truth for your types, and what gets generated from what. Four common answers compete in the Go ecosystem. The OpenAPI-first stack uses an OpenAPI 3 specification as the canonical contract; tools like deepmap/oapi-codegen and ogen-go/ogen generate Go server stubs, client SDKs, and request/response structs from the spec.
The Protobuf-first stack uses .proto files with protoc-gen-go, and while the wire format is binary by default, the generated structs serialize cleanly to JSON via protojson for HTTP gateways. The JSON-Schema-first stack uses quicktype or similar to produce structs from a JSON Schema document, which is useful when the schema exists but the API is event-based or unstructured. The database-first stack — sqlc, SQLBoiler, ent — generates Go types from a SQL schema, treating the database as the canonical model.
A JSON-sample-first generator like the one on this page is the right tool when none of those upstream artifacts exist yet — when you are reverse-engineering a third-party API, prototyping against a fixture, or sketching a struct for a one-off migration. The output is a starting point: paste, review, adjust nullability, decide on pointers, fix anything the sample under-determined, and commit. If the same shape matters for more than a sprint, promote it: write the OpenAPI spec, define the proto, or add the table, and let the schema generate the struct from then on.
Inference is a starting point, not the answer.
One sample can't reveal nullability — if a field happened to be present and non-null in your example, the generator picks a concrete type, but the API may legitimately return null or omit it. The Go convention is to use pointer types (*string) for nullable fields and add ,omitempty to the JSON tag for optional ones. Run the generated code through your linter (go vet, golangci-lint) and tighten types based on the API contract — not the one sample you pasted.
JSON numbers are double-precision floats by spec. Go's default json.Unmarshal will lose precision on 64-bit IDs unless you use a json.Number, json.RawMessage, or a string field. Twitter, Discord, and Snowflake all serialise IDs as strings for this reason.