Skip to content

Commit bbf45c1

Browse files
authored
Feat: add new --all-fields-required flag (#2)
* feat: add `all_fields_required` option * docs: update README for new `--all-fields-required` flag * feat: account for serialization aliases * fix: ensure all reachable models are processed under --all-fields-required Previously, some Pydantic models that were only indirectly referenced (e.g. buried inside an Annotated[Union[...]] field) weren't processed before final schema generation. As a result, their default-valued fields would be optional in the TypeScript output, even when all_fields_required was set. We now recursively walk and collect all reachable models before calling `_clean_json_schema`, ensuring all schemas are properly processed. * fix: use List[str] instead of list[str] for 3.8 compatibility just in case * fix: import Annotated, get_args, and get_origin from typing_extensions for python 3.8 compatibility * feat: mark all fields as required in a simpler way (now works for both v1 and v2 models) - revert last 4 commits (serialization aliases and walking all models) - just add any property names from the schema directly into `required` instead of working with the model fields directly and needing to account for aliases - walking models no longer needed for all-fields-required to apply to all models used; even if `model` is None we still handle the schema * docs: update README now that the flag works for both v1 and v2 * docs: update readme with better named example fields
1 parent e5b6cdd commit bbf45c1

File tree

2 files changed

+107
-18
lines changed

2 files changed

+107
-18
lines changed

README.md

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,37 @@ Useful for any scenario in which python and javascript applications are interact
1212
This tool requires that you have the lovely json2ts CLI utility installed.
1313
Instructions can be found here: https://www.npmjs.com/package/json-schema-to-typescript
1414

15-
### Installation
15+
## Installation
1616

1717
```bash
18-
$ pip install pydantic-to-typescript
18+
pip install pydantic-to-typescript
1919
```
2020

21-
### Pydantic V2 support
21+
## Pydantic V2 support
2222

2323
If you are encountering issues with `pydantic>2`, it is most likely because you're using an old version of `pydantic-to-typescript`.
2424
Run `pip install 'pydantic-to-typescript>2'` and/or add `pydantic-to-typescript>=2` to your project requirements.
2525

26-
### CI/CD
26+
## CI/CD
2727

2828
You can now use `pydantic-to-typescript` to automatically validate and/or update typescript definitions as part of your CI/CD pipeline.
2929

3030
The github action can be found here: https://github.com/marketplace/actions/pydantic-to-typescript.
3131
The available inputs are documented here: https://github.com/phillipdupuis/pydantic-to-typescript/blob/master/action.yml.
3232

33-
### CLI
33+
## CLI
3434

35-
| Prop | Description |
36-
| :------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37-
| ‑‑module | name or filepath of the python module you would like to convert. All the pydantic models within it will be converted to typescript interfaces. Discoverable submodules will also be checked. |
38-
| ‑‑output | name of the file the typescript definitions should be written to. Ex: './frontend/apiTypes.ts' |
39-
| ‑‑exclude | name of a pydantic model which should be omitted from the resulting typescript definitions. This option can be defined multiple times, ex: `--exclude Foo --exclude Bar` to exclude both the Foo and Bar models from the output. |
40-
| ‑‑json2ts‑cmd | optional, the command used to invoke json2ts. The default is 'json2ts'. Specify this if you have it installed locally (ex: 'yarn json2ts') or if the exact path to the executable is required (ex: /myproject/node_modules/bin/json2ts) |
35+
| Prop | Description |
36+
| :-------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37+
| ‑‑module | name or filepath of the python module you would like to convert. All the pydantic models within it will be converted to typescript interfaces. Discoverable submodules will also be checked. |
38+
| ‑‑output | name of the file the typescript definitions should be written to. Ex: './frontend/apiTypes.ts' |
39+
| ‑‑exclude | name of a pydantic model which should be omitted from the resulting typescript definitions. This option can be defined multiple times, ex: `--exclude Foo --exclude Bar` to exclude both the Foo and Bar models from the output. |
40+
| ‑‑json2ts‑cmd | optional, the command used to invoke json2ts. The default is 'json2ts'. Specify this if you have it installed locally (ex: 'yarn json2ts') or if the exact path to the executable is required (ex: /myproject/node_modules/bin/json2ts) |
41+
| ‑‑all‑fields‑required | optional (off by default). Treats all fields as required (present) in the generated TypeScript interfaces. |
4142

4243
---
4344

44-
### Usage
45+
## Usage
4546

4647
Define your pydantic models (ex: /backend/api.py):
4748

@@ -74,13 +75,13 @@ def login(body: LoginCredentials):
7475
Execute the command for converting these models into typescript definitions, via:
7576

7677
```bash
77-
$ pydantic2ts --module backend.api --output ./frontend/apiTypes.ts
78+
pydantic2ts --module backend.api --output ./frontend/apiTypes.ts
7879
```
7980

8081
or:
8182

8283
```bash
83-
$ pydantic2ts --module ./backend/api.py --output ./frontend/apiTypes.ts
84+
pydantic2ts --module ./backend/api.py --output ./frontend/apiTypes.ts
8485
```
8586

8687
or:
@@ -138,3 +139,65 @@ async function login(
138139
}
139140
}
140141
```
142+
143+
### Treating all fields as required
144+
145+
If you would like to treat all fields as required in the generated TypeScript interfaces, you can use the `--all-fields-required` flag.
146+
147+
This is useful, for example, when representing a response from your Python backend API—since Pydantic will populate any missing fields with defaults before sending the response.
148+
149+
#### Example
150+
151+
```python
152+
from pydantic import BaseModel, Field
153+
from typing import Annotated, Literal, Optional
154+
155+
class ExampleModel(BaseModel):
156+
literal_str_with_default: Literal["c"] = "c"
157+
int_with_default: int = 1
158+
int_with_pydantic_default: Annotated[int, Field(default=2)]
159+
int_list_with_default_factory: Annotated[list[int], Field(default_factory=list)]
160+
nullable_int: Optional[int]
161+
nullable_int_with_default: Optional[int] = 3
162+
nullable_int_with_null_default: Optional[int] = None
163+
```
164+
165+
Executing with `--all-fields-required`:
166+
167+
```bash
168+
pydantic2ts --module backend.api --output ./frontend/apiTypes.ts --all-fields-required
169+
```
170+
171+
```ts
172+
export interface ExampleModel {
173+
literal_str_with_default: "c";
174+
int_with_default: number;
175+
int_with_pydantic_default: number;
176+
int_list_with_default_factory: number[];
177+
nullable_int: number | null;
178+
nullable_int_with_default: number | null;
179+
nullable_int_with_null_default: number | null;
180+
}
181+
```
182+
183+
Executing without `--all-fields-required`:
184+
185+
```bash
186+
pydantic2ts --module backend.api --output ./frontend/apiTypes.ts
187+
```
188+
189+
```ts
190+
export interface ExampleModel {
191+
literal_str_with_default?: "c";
192+
int_with_default?: number;
193+
int_with_pydantic_default?: number;
194+
int_list_with_default_factory?: number[];
195+
nullable_int: number | null; // optional if Pydantic V1
196+
nullable_int_with_default?: number | null;
197+
nullable_int_with_null_default?: number | null;
198+
}
199+
```
200+
201+
> [!NOTE]
202+
> If you're using Pydantic V1, `nullable_int` will also be optional (`nullable_int?: number | null`) when executing without `--all-fields-required`. See [Pydantic docs](https://docs.pydantic.dev/2.10/concepts/models/#required-fields):
203+
> > 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.

pydantic2ts/cli/script.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ def _extract_pydantic_models(module: ModuleType) -> List[type]:
158158
return models
159159

160160

161-
def _clean_json_schema(schema: Dict[str, Any], model: Any = None) -> None:
161+
def _clean_json_schema(
162+
schema: Dict[str, Any], model: Any = None, all_fields_required: bool = False
163+
) -> None:
162164
"""
163165
Clean up the resulting JSON schemas via the following steps:
164166
@@ -198,6 +200,16 @@ def _clean_json_schema(schema: Dict[str, Any], model: Any = None) -> None:
198200
exc_info=True,
199201
)
200202

203+
if all_fields_required:
204+
_treat_all_fields_as_required(schema)
205+
206+
207+
def _treat_all_fields_as_required(schema: Dict[str, Any]) -> None:
208+
required_properties = schema.setdefault("required", [])
209+
for prop_name in schema.get("properties", {}).keys():
210+
if prop_name not in required_properties:
211+
required_properties.append(prop_name)
212+
201213

202214
def _clean_output_file(output_filename: str) -> None:
203215
"""
@@ -266,7 +278,7 @@ def _schema_generation_overrides(
266278
setattr(config, key, value)
267279

268280

269-
def _generate_json_schema(models: List[type]) -> str:
281+
def _generate_json_schema(models: List[type], all_fields_required: bool = False) -> str:
270282
"""
271283
Create a top-level '_Master_' model with references to each of the actual models.
272284
Generate the schema for this model, which will include the schemas for all the
@@ -291,7 +303,9 @@ def _generate_json_schema(models: List[type]) -> str:
291303
defs: Dict[str, Any] = master_schema.get(defs_key, {})
292304

293305
for name, schema in defs.items():
294-
_clean_json_schema(schema, models_by_name.get(name))
306+
_clean_json_schema(
307+
schema, models_by_name.get(name), all_fields_required=all_fields_required
308+
)
295309

296310
return json.dumps(master_schema, indent=2)
297311

@@ -301,6 +315,7 @@ def generate_typescript_defs(
301315
output: str,
302316
exclude: Tuple[str, ...] = (),
303317
json2ts_cmd: str = "json2ts",
318+
all_fields_required: bool = False,
304319
) -> None:
305320
"""
306321
Convert the pydantic models in a python module into typescript interfaces.
@@ -313,6 +328,9 @@ def generate_typescript_defs(
313328
:param json2ts_cmd: optional, the command that will execute json2ts.
314329
Provide this if the executable is not discoverable
315330
or if it's locally installed (ex: 'yarn json2ts').
331+
:param all_fields_required: optional, treat all v2 model fields (including
332+
those with defaults) as required in generated
333+
TypeScript definitions.
316334
"""
317335
if " " not in json2ts_cmd and not shutil.which(json2ts_cmd):
318336
raise Exception(
@@ -335,7 +353,7 @@ def generate_typescript_defs(
335353

336354
LOG.info("Generating JSON schema from pydantic models...")
337355

338-
schema = _generate_json_schema(models)
356+
schema = _generate_json_schema(models, all_fields_required=all_fields_required)
339357
schema_dir = mkdtemp()
340358
schema_file_path = os.path.join(schema_dir, "schema.json")
341359

@@ -392,6 +410,13 @@ def parse_cli_args(args: Optional[List[str]] = None) -> argparse.Namespace:
392410
"Provide this if it's not discoverable or if it's only installed locally (example: 'yarn json2ts').\n"
393411
"(default: json2ts)",
394412
)
413+
parser.add_argument(
414+
"--all-fields-required",
415+
action="store_true",
416+
default=False,
417+
help="Treat all fields (including those with defaults) as required in generated TypeScript definitions.\n"
418+
"(Currently supported only for Pydantic V2 models.)",
419+
)
395420
return parser.parse_args(args)
396421

397422

@@ -406,6 +431,7 @@ def main() -> None:
406431
args.output,
407432
tuple(args.exclude),
408433
args.json2ts_cmd,
434+
all_fields_required=args.all_fields_required,
409435
)
410436

411437

0 commit comments

Comments
 (0)