pydantic-to-typescript
pydantic-to-typescript copied to clipboard
Feat: add new `--all-fields-required` flag (addresses #28: Field with default value becomes optional)
Summary
Fixes: #28
Adds an --all-fields-required option (defaults to False) that ensures no generated TypeScript interface fields are marked as optional (fieldName?: ...), even if they have default values or default factories in the Pydantic models.
Motivation
Fields with defaults are viewed as optional by Pydantic, and thus currently become optional in the generated TypeScript interfaces.
This makes sense for API request schemas: clients don't need to provide values for these fields, since Pydantic can populate them with their defaults. However, for response schemas, the TS client should be able to know that these fields will be present, since Pydantic will populate them before sending the response data.
So, this new option (--all-fields-required) allows devs to represent response schemas without unnecessary optional markers (?) being added to fields that will always be present.
Approach
The implementation is nice and simple: at the end of each _clean_json_schema call, if --all-fields-required is set, we ensure every property name in schema["properties"] is included in schema["required"].
Comparison to existing PR #31
An existing PR (#31, filed by @bibermann in 2022) implements a similar flag --readonly-interfaces and is also marked to resolve #28.
It takes a slightly different approach, only operating on Pydantic V1 models (Pydantic V2 didn't exist back then), and marking a field as required if allow_none is true.
Under that approach, fields could still be marked as optional in the generated TS interfaces. E.g. under --readonly-interfaces, this:
class Foo(BaseModel):
required: str
default: str = "foo"
optional: Optional[str]
optional_default: Optional[str] = "foo"
would be generated into this:
export interface Foo {
required: string;
default: string;
optional?: string;
optional_default?: string;
}
Whereas under --all-fields-required it would be generated into this:
export interface Foo {
required: string;
default: string;
optional: string | null;
optional_default: string | null;
}
As we can see, another difference in the outputs is the existence of null types, but that's because of the pydantic2ts v2.0.0 release in Nov 2024.
Note on required fields in Pydantic V1 vs V2
Under Pydantic V1, nullable fields were implicitly given default values of None, which the Pydantic docs mention here:
[!NOTE] In Pydantic V1, fields annotated with Optional or Any would be given an implicit default of None even if no default was explicitly specified. This behavior has changed in Pydantic V2, and there are no longer any type annotations that will result in a field having an implicit default value.
But since --all-fields-required is just meant to mark every field as required whether or not it has a default or is nullable, our approach should be sound for either V1 or V2.