DevKitHub

Programming

JSON to Go Struct Converter

Paste a JSON response to get Go structs for encoding/json, with a json tag on every field. Every element of an array is merged into one struct, so a field that is sometimes null or missing comes out as a pointer.

12 lines
28 lines
  • $.created_at: every sample is an RFC 3339 date-time, so the type is time.Time. Decoding fails on anything else, including "" and a date without a time; use string if the API can send those.

The structs describe the sample you paste: paste an array of several responses so that fields which are sometimes missing or null become pointers. Written for encoding/json and Go 1.18 or later.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

Every value found at the same position is merged into one description before any Go is written, so an array of objects gives one struct typed from all of its elements, not the first. Numbers are where Go is strictest. A field whose samples are all written as integers, with no decimal point or exponent, and fit in int64 is int64; a single 2.5 makes it float64, because encoding/json will not decode 1.0 or 1e3 into an int64 and fails with "cannot unmarshal number 1.0 into Go struct field". An integer beyond int64, or a number beyond float64, is json.Number, which keeps the digits exactly as written. A field whose JSON type differs between samples, such as a number in one and a string in another, is any, with a warning: encoding/json fills an any with float64 for every number, so an ID of 9007199254740993 read into an any comes back as 9007199254740992 unless the Decoder calls UseNumber.

A field that is null in some samples, or missing from some objects, is a pointer such as *string or *Address. Without one, null and a missing key both decode to the zero value, and a real 0, "" or false can no longer be told apart from no value at all; with one, nil means null or absent and anything else is a value. Slices, maps and any are not wrapped, because nil already stands for null there. A key missing from some samples also gets ,omitempty, so marshalling leaves it out instead of sending null. On a pointer omitempty drops only nil, so a present 0 is still written; on a slice it drops an empty one as well, so [] goes back out as a missing key, and ,omitzero (Go 1.24) drops only nil if that difference matters. A string field is time.Time only when every sample is an RFC 3339 date-time with seconds, a capital T and a Z or offset, because that is all time.Time decodes: "", a date on its own or a time without a zone makes the whole Unmarshal fail.

Field names must be exported, so each is PascalCase from its key with golint’s initialisms in capitals: user_id, user-id and userId all give UserID, and avatar_url gives AvatarURL. A name that cannot start an exported identifier gets Field in front, so 2fa gives Field2fa, and keys that produce the same name are numbered UserID2, with a warning. The tag always carries the original key, so marshalling writes the key exactly as the API sent it; decoding still matches keys case-insensitively, as encoding/json always has, so {"ID": 7} also fills a field tagged json:"id". A key that is a single dash is tagged json:"-,", since json:"-" means skip the field, and a key containing a comma, a quote, a backslash or a backquote cannot be named by any tag, so it gets no field and a warning. Nested objects become named types after their keys, shared when two are identical, or inline anonymous structs if you prefer; a root array is a slice type such as type Root []RootItem. The output is laid out as gofmt lays it out, and imports time or encoding/json only when it uses them.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

Unexpected content after the end of the JSON value. A JSON document holds exactly one value; wrap several in an array.

{"level":"INFO","msg":"started"}
{"level":"INFO","msg":"ready"}
Why:
JSON Lines: one document per line, as written by log/slog’s JSONHandler, zap, or a json.Encoder called in a loop. Each line is JSON, but the file as a whole is not one JSON document.
Fix:
Wrap the lines in [ ] with a comma after each; the records are then merged into one struct, which is what a log line type wants.

NaN, Infinity and undefined are JavaScript values with no JSON equivalent.

{"ratio": NaN}
Why:
NaN or Infinity written by a serializer that allows them, such as Python’s json.dumps with its defaults. JSON has no such numbers, and Go’s json.Marshal refuses them too, with an UnsupportedValueError.
Fix:
Replace them with null or a number before pasting. In Go, a *float64 left nil is the usual way to send no value.

json: cannot unmarshal number 1.5 into Go struct field Item.price of type int64

Why:
Every sample of the field happened to be a whole number, so it was typed int64, and the real API also sends fractions. encoding/json will not truncate a number to fit, and refuses 1.0 as well.
Fix:
Paste a sample that includes a fractional value, or change the field to float64 by hand.

parsing time "" as "2006-01-02T15:04:05Z07:00": cannot parse "" as "2006"

Why:
The field is time.Time because every sample was an RFC 3339 date-time, and a real response sends an empty string where there is no date. time.Time decodes RFC 3339 and nothing else, so the whole Unmarshal fails.
Fix:
Turn off the time.Time option to keep the field a string. If the API sends null rather than "", a *time.Time decodes it as nil.

A key from the JSON has no field in the struct.

Why:
The key is empty or contains a comma, a quote, a backslash or a backquote. encoding/json reads a tag up to its first comma and has no escape for these characters, so no tag can name such a key. A warning lists each one.
Fix:
Decode that object into a map[string]any, or give the type an UnmarshalJSON method that reads the key itself.

Frequently asked questions

Why are some fields pointers?
Because the samples show them null, or missing from some objects. A plain string decodes both null and a missing key as "", which cannot be told apart from a real empty string; a *string is nil for those and points at the value otherwise. Check for nil before dereferencing. From Go 1.26, new("value") gives a *string directly when you build one yourself.
Why int64 rather than int?
The size of int depends on the platform: 64 bits on amd64 and arm64, 32 on 386 and 32-bit ARM, where an ID above 2,147,483,647 fails to decode. int64 is the same everywhere. If you know the values are small, int works just as well with encoding/json.
How do I decode JSON into the generated struct?
var v Root, then err := json.Unmarshal(data, &v), or json.NewDecoder(r).Decode(&v) for a request body or a file. Call DisallowUnknownFields on the Decoder to turn a key the struct has no field for into an error rather than silently ignoring it.
Which Go version does the output need?
Go 1.18 or later, for any; replace any with interface{} on older releases. Tag names containing characters other than letters, digits and simple punctuation, such as a key with € in it, are read by encoding/json from Go 1.27, where it is built on the v2 engine; the tool warns about each one, because earlier releases ignore such a tag and use the field name.

Last updated