JSON to Go

Generate Go structs with json tags from JSON: numbers typed by how they are written, pointers where a value may be missing — all in your browser.

Input
Go
type Root struct {
	Orders  []Order `json:"orders"`
	HasMore bool    `json:"has_more"`
}

type Order struct {
	ID        int64    `json:"id"`
	UserID    int64    `json:"user_id"`
	CreatedAt string   `json:"created_at"`
	Price     float64  `json:"price"`
	Coupon    *string  `json:"coupon"`
	GiftNote  *string  `json:"gift_note,omitzero"`
	Shipping  Shipping `json:"shipping"`
	Tags      []any    `json:"tags"`
}

type Shipping struct {
	City     string          `json:"city"`
	Postcode json.RawMessage `json:"postcode"`
}
  • At each position below, every number was read as a whole number, so it is typed int64. A value written with a decimal point or an exponent, even 10.0, will not decode into it.

    Positions: 2

    • Order.ID
    • Order.UserID
  • At each position below, the values are of more than one kind — numbers and strings, say — so it is typed json.RawMessage, which keeps each value as it is written in your JSON, for your program to decode once it knows which kind it is. Where one kind is an object, its struct is still in the output, to decode that value into.

    Positions: 1

    • Shipping.Postcode
  • At each position below, there was nothing to infer a type from: only null was seen there, the array was always empty, the object had no keys, or the value is nested too deeply for this tool to follow. So it is typed any, which accepts any value — or map[string]any where the object had no keys, which accepts any object or null and nothing else.

    Positions: 1

    • Order.Tags[]

Structs that decode the JSON they came from

Paste a sample of JSON and this page writes Go declarations for it: a named struct for each object in the sample, and a json tag on every field naming the key it reads. One promise decides everything below. Put into a program built with Go 1.27, the declarations decode the JSON you pasted through "encoding/json" without an error, and every key is read into a field, except a key that no struct tag can carry, which is left out. Where Go will not let that promise hold, or makes the page choose something your data did not decide, a finding under the output says where, with one rare exception described under the tags.

The shape itself is worked out before any Go is written, by the same reading of your JSON that feeds the JSON to TypeScript page. Combining an array’s elements, noticing keys that only some of them have, separating null from a key that was never sent and naming every object found inside another: that page’s guide covers all of it, so none of it is repeated below. What follows is Go’s part — a type per number, pointers, a type for a key with values of different kinds, field names, tags, and the questions the findings raise.

Three number types, chosen from how each number is written

A browser reading JSON turns 10 and 10.0 into the same number. Go does not: an "int64" field refuses a number written with a decimal point or an exponent — 10.0, 1e3, even -0.0 — while it takes 11 and -0. So the page does not choose a number type from the values alone. It asks the browser’s own parser how each number was written, and decides from that:

  • Where every number at a position is written as a whole number, with no decimal point and no exponent, the field is "int64", and the finding for whole numbers names it.
  • Any other number is "float64", so a price written 10.0 is "float64" even when every price in the sample is round. Python’s "json" module writes a whole-valued float exactly that way, which makes an API served by Python the usual place to meet it.
  • "json.Number" is kept for the numbers neither type holds exactly: a whole number past either end of the range of "int64", a number too large for a "float64" at all, such as 1e999, and a whole number too long for a "float64" beside a fraction, as in [9007199254740993, 1.5]. It holds each of them exactly, and your program converts it where it uses the value.

A long whole number among other whole numbers needs no such care: [9007199254740993, 1] is "[]int64". A fraction with more digits than a "float64" keeps is left as "float64" and named in a finding of its own, since decoded and encoded again, 0.30000000000000001 comes back as 0.3.

Reading how a number was written needs a browser that reports it. One that does not reads each number from its value alone, so 10.0 looks whole there and its field becomes "int64", which will not decode that JSON; the page counts the numbers it could not tell from whole ones and shows where the first is. Such a browser also cannot see the digits a "float64" drops, so there a long whole number beside a fraction is typed "float64", and a fraction that rounds is not named.

Pointers where a value may be missing

A key that some objects leave out, or set to null, is a pointer: "*string", "*int64", or a pointer to the struct written for an object. The elements of an array holding null are pointers too, so [1, null] is "[]*int64". A key left out of some objects is also marked "omitzero" in its tag. With both in place a field can say whether a value arrived, and encoding the decoded struct again writes a key the sample only ever left out back as missing, and a key it only ever set to null back as null.

A slice, a map, "any" and "json.RawMessage" take no pointer, since each is already nil when nothing was decoded into it. A slice still gets "omitzero" where its key may be missing, and an array that was present but empty is written back as [].

One distinction does not survive. A key missing from some objects and null in others is a single nil in Go, so your program cannot tell the two apart, and encoding the struct again writes the key as missing even where the sample had null; the page names every field this happens to. A key of mixed kinds keeps the difference, because a "json.RawMessage" holds a null as the bytes null.

json.RawMessage for mixed kinds, and any where nothing was seen

When a key’s values disagree in kind — a number in one place, a string or an object in another — the field is "json.RawMessage": the value’s own bytes, left for your program to decode once it has looked at which kind arrived. A null among them changes nothing. Where one of the kinds is an object, the struct extracted for it is still printed beside the others. An "any" would accept every kind as well, and it is not used for a mix, because a number decoded into an "any" becomes a "float64" and loses whatever a "float64" cannot hold.

Some parts of a sample offer no value to learn from: a key that only ever held null, an array that was empty every time, an object without keys, and a value buried deeper than the inference reads. Go gets "any" for the null key and the buried value, "[]any" for the empty array’s elements and "map[string]any" for the object. An "any" takes every kind of value. The map takes an object or null, and a string, a number, an array or a boolean in its place makes decoding fail.

Past that depth two things go unsaid by the findings. A key there that is missing from some objects and null in others is named only as a position with nothing to infer, and Go writes its null back as missing. And a whole number longer than a "float64" holds comes back rounded, 9007199254740993 returning as 9007199254740992, because the position is an "any".

Field names in Go’s style, struct names shared with the TypeScript page

A field’s name is Go’s spelling of its key. The key is split into words at every character that is not a letter or a digit, and wherever a lower-case letter is followed by a capital; the words are then joined, each starting with a capital, so "user_name", "last-name" and "firstName" become UserName, LastName and FirstName. A word on staticcheck’s default list of initialisms is written in capitals — "id" is ID, "api_key" is APIKey, "video_url" is VideoURL — but only a whole word counts, so "idle" is Idle and the plural "ids" is Ids. A key written entirely in capitals is read as words: "USER_ID" is UserID.

The key’s own letters are kept, so "имя" becomes Имя. Where a name would not start with a capital letter — a key in a script without capitals such as "名前", a key starting with a digit such as "1st", or a first letter with no one-letter capital such as "ß" — it takes an X in front, as X名前, X1st and Xß, and the field reads its key all the same. Combining marks are left out of the name and kept in the tag. A key with no letter or digit at all, such as "@", is called Field, and a name already used in the same struct takes a number: "user_id", "userId" and "USER_ID" together are UserID, UserID2 and UserID3.

Go’s style reaches field names and stops there: every struct keeps the name the inference chose for its object, so the declarations here and the interfaces on the TypeScript page are named alike, and a field can be spelled differently from the struct it holds. How that name is chosen belongs to the TypeScript guide.

Tags for Go 1.27, and the keys no tag can carry

Each field’s tag names its key exactly, as in json:"user_id", with ,omitzero after the key where the key may be missing. A control character in a key is written with the escape Go itself uses, and the key "-" is written json:"-,", a form Go reads as that key.

The tags are written for Go 1.27. Go 1.26 reads fewer keys through a tag: a key holding anything besides letters, digits, the ASCII space and a set of ASCII punctuation — "Price (€)", "temp °C", an emoji, a control character, a combining accent — is not read there, and the page names each such field. That half is checked against Go 1.27 built with "GOEXPERIMENT=nojsonv2", which reads tags as Go 1.26 does but with Go 1.27’s newer Unicode tables, so a key holding a letter that Go 1.26’s older tables do not have is read in that check and raises no finding.

A key holding a comma, a backslash, a quotation mark, an apostrophe or a backtick, or the empty key, is one no struct tag can carry, so it gets no field at all. Your JSON still decodes: "json.Unmarshal" skips that key without complaint, although a "json.Decoder" set to "DisallowUnknownFields" stops at it. The finding lists such a key by its struct and the key quoted as Go quotes a string, as Order["note,internal"], and an object under such a key still has its struct printed. One case goes unflagged: two keys that differ only in a lone surrogate, an escape that stands for no character, are one key to Go, and neither of their fields is filled.

Declarations only, in gofmt’s layout

The output holds type declarations and nothing else: no package clause and no import, so it goes into a file you already have, under that file’s own package clause. Where it uses "json.RawMessage" or "json.Number", the file also imports "encoding/json", which is the one line left to you or to your editor. The layout is gofmt’s own — a tab before each field, and each struct’s names, types and tags in aligned columns — so gofmt leaves it exactly as it is. The root’s type is printed first, every object is a named type rather than a struct written inline, and JSON that is an array or a single value at the top level is a named type as well, such as "type Root []RootItem", so there is always a type to decode it into.

What the findings ask you to check

Each kind of decision Go forced gets one entry below the declarations, and the entry gathers every place it applies to, so a sample with many whole-number fields gets one entry and not one per field. Places are spelled as the declarations spell them — Order.UserID is a field, Order.Tags[] the elements of an array, and the root is its bare name — except in two entries that point into your JSON by line and column instead: a key written twice, and numbers this browser could not tell from whole ones. None of them alters the declarations. Read each as a question about your data:

  • Whole numbers typed "int64". Every number there was written whole. If a value that arrives later may carry a decimal point or an exponent, as a price or a measurement can, it will not decode, so make that field "float64"; an ID or a count can stay as it is.
  • "json.Number". No other number type holds those values exactly. Convert each where your program uses it, or, if you know the real values fit a narrower type, change the field yourself.
  • A fraction "float64" rounds. A number there has more digits than a "float64" keeps, so your program sees a nearby value rather than the one written. Where every digit matters, make the field "json.Number".
  • Mixed kinds kept as "json.RawMessage". Look at each value as it arrives and decode it as the kind it turns out to be; where one kind is an object, its struct is still in the output, to decode that value into.
  • Nothing to infer a type from. The field is "any", or a map of "any", because the sample held no value there to learn from. Replace it with the type you know that field carries, or convert again from JSON where it holds real values.
  • Missing in some objects, null in others. Go keeps one nil for both, so the key is written back as missing. That matters only if whatever reads your output treats a null differently from a key that is not there.
  • Keys Go 1.26 does not read. Build with Go 1.27, or rename those keys where they are produced; on Go 1.26 they go unread.
  • Keys left out. They have no field. To read one, decode the object into a "map[string]json.RawMessage" or give the struct an "UnmarshalJSON" method, as the finding itself suggests.
  • A key written twice. The types follow the key’s last copy, but Go decodes every copy in turn, so an earlier copy the type cannot hold, such as a string where the last copy is a number, makes decoding return an error, and a key that only an earlier copy of an object holds is not read. Fix the JSON at the line and column shown.
  • This browser cannot tell 10 from 10.0. It does not report how a number is written, so a field typed "int64" from a value written with a decimal point or an exponent will not decode your JSON. Check the fields written that way, or convert the JSON in a browser that reports it.

There is no finding about dates, formats or fixed sets of values, because the page never guesses one from a string: "created_at" in the example stays a "string", whatever it looks like.

Frequently asked questions

Why is my price float64 when every price in my JSON is a round number?
Because each price is written with a decimal point, as 10.0, and an "int64" field refuses a number written that way, round or not. The page reads how every number is written rather than only its value, so the field is "float64" and your JSON decodes. Typed "int64" from the values alone, it would stop at the first price.
When does a number come out as json.Number?
When one of its values is a whole number past the range of "int64", a number too large for a "float64", or a long whole number sitting beside a fraction, which a "float64" would round. "json.Number" keeps every one of them exact, and the finding under the output lists each field it was chosen for.
Why are some fields pointers, and what is omitzero doing in the tags?
A field is a pointer where its key is missing from some objects or null in some, so a nil can say that no value arrived. "omitzero" goes where the key is missing from some objects: encoding the struct again then leaves that key out, as your JSON did, instead of writing null for it.
Why json.RawMessage for a key of mixed kinds, and not any?
A "json.RawMessage" keeps each value’s bytes until your program has decided how to read them. An "any" would accept the values too, but a number decoded into an "any" is a "float64", and a long whole number loses its last digits there.
Why do some field names start with an X?
Because otherwise the name would not start with a capital letter: the key is written in a script without capitals, starts with a digit, or starts with a letter that has no one-letter capital form. The X keeps the key’s own letters in the name, and the field still reads its key — X名前 reads "名前".
Which Go version do the structs need?
They are written and checked for Go 1.27. How Go 1.26 reads the tags is checked too, through a stand-in: Go 1.26 does not read a key holding a character outside letters, digits, the ASCII space and a set of ASCII punctuation, such as a currency sign or an emoji, and the page names every field whose key that affects — save a key holding a letter newer than Go 1.26’s Unicode tables: neither the page nor that check can see such a key.
Why is one of my keys missing from the struct?
Because the key holds a comma, a backslash, a quotation mark, an apostrophe or a backtick, or is empty, and no struct tag can carry a key like that. A finding lists it by its struct and the key itself, as Root["a,b"]; the rest of your JSON still decodes, and the finding says how to read that key another way.
Is it safe to paste a response that holds tokens or passwords?
Yes. Every step runs inside this page on your computer, and the response you paste goes nowhere else: no server receives it and nothing keeps a copy. The declarations it produces carry field names, tags and Go types only, so a token or a password in the JSON leaves no trace in them beyond a "string" field named after its key.

Related tools

  • JSON to TypeScript

    Where a key’s values differ in kind, this page keeps each one as it is written in your JSON for your program to decode, since Go has no union. That page writes the same shape as TypeScript types under the same type names, with a union there, and its guide explains how that shape is worked out.

  • JSON to Zod

    Go has no type that is simply a JSON number, so where every number is written whole this page types the field as an integer, and a value written with a decimal point will not decode into it. That page writes the same shape as Zod schemas under the same type names, and a schema checks the data each time it arrives and accepts a fraction in such a field too.

  • JSONPath tester

    Test RFC 9535 JSONPath queries against JSON.

  • Markdown table generator

    Build and align Markdown tables from CSV, TSV or JSON.