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 keysGo fieldsPython 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:

LanguageKeys inFields outLost
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 keyPython fieldPasses the identifier checkResult
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:

LanguageGeneratedOriginal 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 keyTypeScript
first name"first name": T;
a-b"a-b": T;
2fa"2fa": T;
classclass: T;
ok_nameok_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

  1. Paste a representative JSON sample.
  2. Choose your target language.
  3. Review the generated types.
  4. 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.