Three decisions that make the output usable
Array elements are merged, not sampled. A generator that types an array from its first element produces a model missing every field that element happened to lack. Every element is unified here instead, and a field absent from some of them becomes optional - which is almost always the truth about the API that produced the sample.
Identical shapes are shared. An object with a from and a to carrying the same fields produces one type used twice, not two identical declarations under different names. Structural identity is decided on the field list and the field types, so the output stays as small as the data actually is.
Nullable and optional are different facts. Optional means the key was absent from some element; nullable means it was present and its value was null. A field that is always there and sometimes null is a nullable string, not a required one. Conflating the two produces a model that type-checks and then throws on the first null, and it is the commonest defect in generators of this kind.
Why the Java output is a record
The well-known converters own the TypeScript query, and none of them emits a Java record - the shape a codebase written after Java 16 actually wants. A bean with thirty lines of getters is not a model, it is a chore.
So the Java emitter produces a record with Jackson annotations wherever the wire name differs from the Java one, which for a snake_case API is most fields. Kotlin gets a data class with kotlinx.serialization annotations on the same principle.
- TypeScript interfaces, with optional and nullable spelled separately
- Java records, annotated only where the names actually differ
- Kotlin data classes with SerialName and nullable types
- Go structs with json tags, and a pointer where the field is nullable
- Python dataclasses, with defaulted fields ordered after required ones
Naming
Only the root type needs a name. Everything else is named after the field that holds it, singularised for an array - a bookings array of objects produces a Booking. That is what makes the output read like something a person wrote rather than a numbered list of anonymous types.
What it cannot know
A model generated from one sample describes that sample. A field that was only ever null in it has no knowable type, and is emitted as the target’s open type with a warning rather than guessed at.
An integer that happens to have no decimal part is typed as an integer, which is wrong if the API can return a fraction. A one-element array makes every field look required. And an id above two to the fifty-third has already been rounded by the JSON parser before the generator sees it, so a very large numeric id is better modelled as a string.
It does not read an OpenAPI document, infer enums from repeated strings, or detect a date inside a string. Read the generated types before you commit them.