JSON to TypeScript, Go and Python
Turn sample JSON into TypeScript interfaces, Go structs or Python dataclasses, with sensible naming and nullable handling for uncertain fields.
Nineteen JSON keys, nine Go fields
JSON keys can be any string. Programming languages want identifiers, which are a much smaller set, so every generator has to rename. That renaming is many-to-one, and that is where the trouble is:
| JSON keys | Go fields | Python fields |
|---|---|---|
firstName, first_name, first name, first-name, First Name | FirstName | first_name |
userID, user_id, user id | UserID, UserId | user_id |
a.b, a b, a-b, a_b | AB | a_b |
Five distinct keys, one field. Nothing in the generated code records that the other four existed. Over a realistic set of 19 keys — the mixture you get when a payload has grown across several teams:
| Language | Keys in | Fields out | Lost |
|---|---|---|---|
| Go | 19 | 9 | 10 |
| Python | 19 | 8 | 11 |
Roughly half the schema, gone quietly, in a struct that compiles perfectly. It is worth being precise about whose fault that is: the rules here are the conventional ones and they are doing exactly what they were asked to. Any function from "any string" to "valid identifier" that also normalises word boundaries must collapse some inputs together. A generator can warn about it; it cannot avoid it.
And it is specifically about mixed styles, not about renaming. The same rules applied to 5 consistently-named keys lose nothing at all. So the practical check is: if a payload mixes naming conventions, count the generated fields against the keys before trusting the output.
A collision compiles. A reserved word does not.
The sharper failure, and at least a loud one. A JSON key of class or
from is perfectly legal, and snake_case leaves it exactly as it was:
| JSON key | Python field | Passes the identifier check | Result |
|---|---|---|---|
class | class | yes | will not parse |
from | from | yes | will not parse |
import | import | yes | will not parse |
type | type | yes | fine |
id | id | yes | fine |
lambda | lambda | yes | will not parse |
return | return | yes | will not parse |
global | global | yes | will not parse |
All 6 of the keyword cases pass the identifier check, because being
a valid identifier and being available to use are different questions and a
pattern like /^[A-Za-z_][A-Za-z0-9_]*$/ only answers the first. If your payload
has a key named after a keyword, the generated Python is a syntax error and you will find out
when you run it.
Only two of the three outputs can be mapped back
This is the most useful thing to know when picking a target, and it is a property of the
languages rather than of this page. Take the awkward key first name:
| Language | Generated | Original key kept |
|---|---|---|
| Go | FirstName string `json:"first name"` | in a struct tag |
| TypeScript | "first name": string; | quoted, because a property name need not be an identifier |
| Python | first_name: str | nowhere — the original key is gone |
2 of the 3 are lossless about the key. Go annotates the rename in a struct tag, so the decoder still matches the wire format and a human can see what happened. TypeScript simply quotes it, because a property name does not have to be an identifier at all — which is why it needs no renaming rule and has no collisions to speak of:
| JSON key | TypeScript |
|---|---|
first name | "first name": T; |
a-b | "a-b": T; |
2fa | "2fa": T; |
class | class: T; |
ok_name | ok_name: T; |
Python keeps nothing. If you are generating Python from a payload with awkward keys, the mapping back to the wire format is something you have to write yourself — and it is worth writing it while you still have the original JSON in front of you.
How to use
- Paste a representative JSON sample.
- Choose your target language.
- Review the generated types.
- Correct optional and nullable fields the sample could not reveal.
Frequently asked questions
How accurate is generated code?
As accurate as your sample is representative, and no more. A single example cannot show which fields are optional, which may be null, or which arrays can be empty. The generated types are a strong starting point that needs a human pass before being trusted.
Why does everything come out as required?
Because a field present in the sample looks required to a generator. In practice most APIs have optional fields, and the only way to know which is documentation or several samples. This is the single most common correction needed on generated types.
How are numbers handled?
JSON has one numeric type, so a generator has to guess between integer and floating point from the values it sees. A field that happens to be 5 in the sample generates as an integer and then breaks on 5.5. Go and Python are stricter about this than TypeScript, which uses one number type throughout.
What happens to nested objects?
They become nested types, named after the field that contained them. Naming is the weak point — a generic field name produces a generic type name, and deeply nested structures produce a pile of types that usually want renaming to something meaningful.
Should I use generated types for an external API?
Yes, as a base, with the caveat that they describe what the API sent once rather than what it guarantees. Where an official schema or client library exists, prefer it — it encodes the contract rather than a snapshot.
What about very large numbers?
A genuine hazard. Integers beyond 53 bits lose precision when parsed as JSON numbers in JavaScript, so a 64-bit identifier can arrive subtly wrong. Well-designed APIs send such values as strings, and generated types should keep them that way rather than converting.
🔒 This tool runs entirely in your browser. Nothing you enter is uploaded, logged, or stored.