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. id should be the key.
  • paid is an INT because MySQL has no real boolean — TINYINT(1) is the idiomatic form.
  • notes is an INT for the usual reason: the sample was null, so there was nothing to infer from. It should be TEXT.
  • created_at is VARCHAR(255), not DATETIME — you lose date arithmetic, sorting is lexicographic, and the column is several times larger than it needs to be.
  • total is DOUBLE, which is the wrong type for money in the same way float64 was. Use DECIMAL(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

UnknownWhy the generator cannot knowHow to find out
Optional fieldsA field present once looks mandatory; an absent one does not exist at allThe API docs, or a few hundred real responses
Nullable fieldsA null carries no type — hence interface, Mixed and INT for the same fieldAsk what the field holds when it is not null
Numeric intent24.5 is a float; whether it is money, a rating or a measurement is invisibleDecimal for currency, integer minor units, or a documented unit
Identifier strings"02134" looks like a number or a date to a heuristicForce string, and never let a ZIP or an account number become numeric
Large integersA 64-bit ID has already lost precision by the time it is JSONTransport as a string; store as bigint
Empty arrays[] has no element typeFind a sample with items in it
Enums“pending” is just a string in one exampleThe docs; then a real enum or a check constraint
RelationshipsA nested object might be an embedded value or another tableYour own modelling decision, not the generator’s

A workflow that produces something trustworthy

  1. 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.
  2. 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.
  3. Fix the four usual suspects: nulls, money, dates, and identifier-shaped strings.
  4. Add what only you know: required, unique, lengths, indexes, defaults, enums, foreign keys.
  5. Rename the placeholders. struct_name, table_name and AutogeneratedMainType should not survive to your first commit.
  6. 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 null field as untyped and fix it by hand — that is where interface, Mixed and INT come 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, required in Mongoose, NOT NULL and 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.

More developer guides