From a JSON sample to typed code: Go structs, GraphQL SDL, Mongoose schemas and SQL tables
You have a JSON response from an API and you need a Go struct, a GraphQL schema, a Mongoose model or a database table for it. Typing that by hand is tedious and error-prone, so you paste it into a generator — and the output looks authoritative in a way it has not earned. A generator sees exactly one example. It cannot know which fields are optional, whether that number is money, whether that string of digits is an identifier that must never be arithmetic, or whether an empty array means “no items” or “we could not tell you the type”. This guide shows what the Go, GraphQL, Mongoose, MySQL, PHP and JavaScript converters here actually produce for one ordinary record — every output below is real, not illustrative — and exactly which parts you must fix before shipping.
One record, six outputs
This is the sample. Nothing exotic: an order, with a couple of the traps that occur in real data.
{
"id": 1042,
"customer_name": "Jane Doe",
"total": 24.5,
"paid": true,
"notes": null,
"created_at": "2026-03-18T09:30:00Z",
"tags": ["priority", "repeat"],
"address": { "city": "Boston", "zip": "02134" }
}All six pages read input with JSON5 rather than strict JSON, so you can paste a snippet that still has trailing commas, comments or unquoted keys from wherever you copied it. Four of them — Go, GraphQL, Mongoose and TOML — take the first element if you paste a top-level array, because a type describes one record.
Go
type StructName struct {
ID int `json:"id"`
CustomerName string `json:"customer_name"`
Total float64 `json:"total"`
Paid bool `json:"paid"`
Notes interface{} `json:"notes"`
CreatedAt time.Time `json:"created_at"`
Tags []string `json:"tags"`
Address Address `json:"address"`
}
type Address struct {
City string `json:"city"`
Zip string `json:"zip"`
}This is the best of the six, and still needs four edits. StructName is a placeholder — rename it and every reference. Notes came back as interface purely because the sample value was null; it is almost certainly *string. CreatedAt was inferred as time.Time from the shape of the string, which is a genuinely clever guess and requires the time import the snippet does not include. And Total float64 is right for JSON and wrong for money — floats do not add up to what accountants expect, so a currency amount wants a decimal type or integer minor units.
Note what it got right: Zip is a string. Hold on to that, because the next generator disagrees.
Mongoose
{
id: { type: 'Number' },
customer_name: { type: 'String' },
total: { type: 'Number' },
paid: { type: 'Boolean' },
notes: { type: 'Mixed' },
created_at: { type: 'Date' },
tags: { type: ['String'] },
address: {
city: { type: 'String' },
zip: { type: 'Date' },
},
}Read that last line again. zip: "02134" — a five-character string of digits — was inferred as a Date. That is not a hypothetical; it is what this generator produces for that input, and it is the clearest possible demonstration of why generated types are a first draft. A Mongoose model with a Date there will reject every document you try to save, and the error will point at Mongoose rather than at the generator that wrote it.
notes became Mixed, which is Mongoose’s way of saying “anything” and disables change tracking — fix it to the real type. The nested address is emitted as an inline object rather than a subdocument schema, which works but skips validation on the nested fields. And nothing is marked required, unique, index or given a default, because a sample cannot express any of that.
GraphQL
type Address {
city: String
zip: String
}
type AutogeneratedMainType {
id: Int
customer_name: String
total: Float
paid: Boolean
notes: String
created_at: String
address: Address
tags: [String]
}Three things to fix. AutogeneratedMainType is a name only a generator could love. Every field is nullable — GraphQL marks non-null with !, and a sample cannot possibly know which fields qualify, so you get the permissive version and have to add ID!, String! and [String!]! yourself. And notes was guessed as String from a null, which is a coin flip.
Also note created_at: String. GraphQL has no date scalar in the specification, so a timestamp is a string unless you define a custom DateTime scalar — worth doing early, because retrofitting it across a schema is tedious. Field names stay snake_case here; most GraphQL codebases would want camelCase, and the case converter will do that pass in one step.
MySQL
The MySQL page is different from the others: it emits a CREATE TABLE from the first object and an INSERT covering every record you pasted, which makes it genuinely useful for turning an API response into seed data. It is also the strictest about shape — arrays and nested objects have no natural column, so this is the one place a record like ours does not convert at all. Give it the flat fields only and you get:
CREATE TABLE IF NOT EXISTS `table_name` (
`id` INT (10) DEFAULT NULL,
`customer_name` VARCHAR (255) DEFAULT NULL,
`total` DOUBLE DEFAULT NULL,
`paid` INT (10) DEFAULT NULL,
`notes` INT (10) DEFAULT NULL,
`created_at` VARCHAR (255) DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;Every line of that needs attention before it goes anywhere near a real database:
- No primary key, no indexes, no
NOT NULL.idshould be the key. paidis an INT because MySQL has no real boolean —TINYINT(1)is the idiomatic form.notesis an INT for the usual reason: the sample was null, so there was nothing to infer from. It should beTEXT.created_atis VARCHAR(255), notDATETIME— you lose date arithmetic, sorting is lexicographic, and the column is several times larger than it needs to be.totalis DOUBLE, which is the wrong type for money in the same wayfloat64was. UseDECIMAL(10,2).VARCHAR(255)everywhere is a default, not a measurement. Names, emails and URLs all have different sensible lengths.
And when the pasted JSON does contain an array or nested object, the page answers //Invalid JSON rather than guessing — which is the honest behaviour, if a slightly opaque message. Flatten first, and decide deliberately whether tags becomes a JSON column, a comma-separated string (please do not) or a proper join table.
PHP and JavaScript
These two are not type generators — they are literal converters, and the distinction matters. PHP output uses short array syntax and preserves every type exactly:
[
'id' => 1042,
'customer_name' => 'Jane Doe',
'total' => 24.5,
'paid' => true,
'notes' => null,
'tags' => [
'priority'
],
'address' => [
'city' => 'Boston',
'zip' => '02134'
]
]That is a faithful translation with nothing guessed, which makes it ideal for turning a fixture, a config block or an API response into code you can paste into a test. (One implementation detail: for inputs over about 5,000 characters the page switches to compact output rather than pretty-printing, because formatting large payloads in the browser gets slow.)
The JavaScript page emits JSON5: unquoted keys where they are valid identifiers, single quotes, trailing commas. Useful for dropping a payload into source code where a bare JSON blob would look out of place — and a reminder that JSON5 is a convenience for humans reading and writing code, not something to send over the wire.
What a single sample can never tell you
| Unknown | Why the generator cannot know | How to find out |
|---|---|---|
| Optional fields | A field present once looks mandatory; an absent one does not exist at all | The API docs, or a few hundred real responses |
| Nullable fields | A null carries no type — hence interface, Mixed and INT for the same field | Ask what the field holds when it is not null |
| Numeric intent | 24.5 is a float; whether it is money, a rating or a measurement is invisible | Decimal for currency, integer minor units, or a documented unit |
| Identifier strings | "02134" looks like a number or a date to a heuristic | Force string, and never let a ZIP or an account number become numeric |
| Large integers | A 64-bit ID has already lost precision by the time it is JSON | Transport as a string; store as bigint |
| Empty arrays | [] has no element type | Find a sample with items in it |
| Enums | “pending” is just a string in one example | The docs; then a real enum or a check constraint |
| Relationships | A nested object might be an embedded value or another table | Your own modelling decision, not the generator’s |
A workflow that produces something trustworthy
- Pick a fat sample, not a tidy one. Find the response with the most fields populated, the arrays non-empty and the optional blocks present. A minimal example produces a minimal type.
- Convert, then read every line. The point of the generator is to save typing, not thinking. Assume every guessed type is wrong until you have looked at it.
- Fix the four usual suspects: nulls, money, dates, and identifier-shaped strings.
- Add what only you know: required, unique, lengths, indexes, defaults, enums, foreign keys.
- Rename the placeholders.
struct_name,table_nameandAutogeneratedMainTypeshould not survive to your first commit. - Round-trip a real payload through the type and confirm nothing is dropped or mangled. That is the test that catches the ZIP-code-as-Date class of bug before it reaches anyone else.
One practical note about all six pages: the conversion happens in your browser. That matters here more than it might seem, because the JSON you paste to generate a type is usually a real API response — with a real customer name, a real address, and sometimes a token in a header you forgot to strip.
Do this
- Feed the generator your fullest sample, not your smallest.
- Treat every
nullfield as untyped and fix it by hand — that is whereinterface,MixedandINTcome from. - Never leave money as a float or a double. Decimal, or integer minor units.
- Force identifier-shaped strings to string types: ZIPs, phone numbers, account codes, anything with a leading zero.
- Add nullability and constraints yourself —
!in GraphQL,requiredin Mongoose,NOT NULLand a primary key in SQL. - Flatten records before the MySQL converter, and decide consciously what happens to nested arrays.
- Rename every placeholder before committing, and round-trip a real payload through the result.
Frequently asked questions
Why did my ZIP code become a Date?
Because a generator only sees one value and has to guess. Given "02134" the Mongoose generator infers Date — a real result you can reproduce — while the Go generator infers string for the same field. Generators guess types from the shape of a single sample, and identifier-like strings are exactly where they guess wrong. Always read the generated types before using them.
Why does the JSON to MySQL converter reject my JSON?
Because the object contains an array or a nested object. A table row is flat, and the library behind that page raises an error rather than inventing a mapping, which the page reports as an invalid-JSON message. Flatten the record first, or decide deliberately whether the nested part becomes a JSON column or a second table with a foreign key.
Why does converting an array of records only produce one type?
The Go, GraphQL and Mongoose pages take the first element of a top-level array and describe that, because a type is a description of one record. The MySQL page is the exception: it builds the CREATE TABLE from the first object and then an INSERT covering every row. If your records vary in shape, the first one is not representative and neither is the generated type.
Can a generated type tell me which fields are optional?
No. Nullability is the single biggest thing missing from a sample. A field that is present in the sample looks required; one that happens to be null looks like it has no type at all. Only the API’s documentation, or a large enough set of responses, tells you which fields can be absent — and that is exactly what causes runtime failures months later.
Is a generated schema safe to put straight into production?
Treat it as scaffolding that saves twenty minutes of typing, not as a contract. Fix the types the generator guessed, add the constraints it cannot know (required, unique, lengths, indexes), name the things it named generically, and check the numbers — money as a decimal, 64-bit IDs as strings or bigints. Then it is production code.
Tools used in this guide
Every one of these runs in your browser — the files you work on never leave your device.