Skip to content

Feat: add new --all-fields-required flag #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 11, 2025
91 changes: 77 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,37 @@ Useful for any scenario in which python and javascript applications are interact
This tool requires that you have the lovely json2ts CLI utility installed.
Instructions can be found here: https://www.npmjs.com/package/json-schema-to-typescript

### Installation
## Installation

```bash
$ pip install pydantic-to-typescript
pip install pydantic-to-typescript
```

### Pydantic V2 support
## Pydantic V2 support

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

### CI/CD
## CI/CD

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

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

### CLI
## CLI

| Prop | Description |
| :------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ‑‑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. |
| ‑‑output | name of the file the typescript definitions should be written to. Ex: './frontend/apiTypes.ts' |
| ‑‑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. |
| ‑‑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) |
| Prop | Description |
| :-------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ‑‑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. |
| ‑‑output | name of the file the typescript definitions should be written to. Ex: './frontend/apiTypes.ts' |
| ‑‑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. |
| ‑‑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) |
| ‑‑all‑fields‑required | optional (off by default). Treats all fields as required (present) in the generated TypeScript interfaces. |

---

### Usage
## Usage

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

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

```bash
$ pydantic2ts --module backend.api --output ./frontend/apiTypes.ts
pydantic2ts --module backend.api --output ./frontend/apiTypes.ts
```

or:

```bash
$ pydantic2ts --module ./backend/api.py --output ./frontend/apiTypes.ts
pydantic2ts --module ./backend/api.py --output ./frontend/apiTypes.ts
```

or:
Expand Down Expand Up @@ -138,3 +139,65 @@ async function login(
}
}
```

### Treating all fields as required

If you would like to treat all fields as required in the generated TypeScript interfaces, you can use the `--all-fields-required` flag.

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.

#### Example

```python
from pydantic import BaseModel, Field
from typing import Annotated, Literal, Optional

class ExampleModel(BaseModel):
literal_str_with_default: Literal["c"] = "c"
int_with_default: int = 1
int_with_pydantic_default: Annotated[int, Field(default=2)]
int_list_with_default_factory: Annotated[list[int], Field(default_factory=list)]
nullable_int: Optional[int]
nullable_int_with_default: Optional[int] = 3
nullable_int_with_null_default: Optional[int] = None
```

Executing with `--all-fields-required`:

```bash
pydantic2ts --module backend.api --output ./frontend/apiTypes.ts --all-fields-required
```

```ts
export interface ExampleModel {
literal_str_with_default: "c";
int_with_default: number;
int_with_pydantic_default: number;
int_list_with_default_factory: number[];
nullable_int: number | null;
nullable_int_with_default: number | null;
nullable_int_with_null_default: number | null;
}
```

Executing without `--all-fields-required`:

```bash
pydantic2ts --module backend.api --output ./frontend/apiTypes.ts
```

```ts
export interface ExampleModel {
literal_str_with_default?: "c";
int_with_default?: number;
int_with_pydantic_default?: number;
int_list_with_default_factory?: number[];
nullable_int: number | null; // optional if Pydantic V1
nullable_int_with_default?: number | null;
nullable_int_with_null_default?: number | null;
}
```

> [!NOTE]
> 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):
> > 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.
34 changes: 30 additions & 4 deletions pydantic2ts/cli/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ def _extract_pydantic_models(module: ModuleType) -> List[type]:
return models


def _clean_json_schema(schema: Dict[str, Any], model: Any = None) -> None:
def _clean_json_schema(
schema: Dict[str, Any], model: Any = None, all_fields_required: bool = False
) -> None:
"""
Clean up the resulting JSON schemas via the following steps:

Expand Down Expand Up @@ -198,6 +200,16 @@ def _clean_json_schema(schema: Dict[str, Any], model: Any = None) -> None:
exc_info=True,
)

if all_fields_required:
_treat_all_fields_as_required(schema)


def _treat_all_fields_as_required(schema: Dict[str, Any]) -> None:
required_properties = schema.setdefault("required", [])
for prop_name in schema.get("properties", {}).keys():
if prop_name not in required_properties:
required_properties.append(prop_name)


def _clean_output_file(output_filename: str) -> None:
"""
Expand Down Expand Up @@ -266,7 +278,7 @@ def _schema_generation_overrides(
setattr(config, key, value)


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

for name, schema in defs.items():
_clean_json_schema(schema, models_by_name.get(name))
_clean_json_schema(
schema, models_by_name.get(name), all_fields_required=all_fields_required
)

return json.dumps(master_schema, indent=2)

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

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

schema = _generate_json_schema(models)
schema = _generate_json_schema(models, all_fields_required=all_fields_required)
schema_dir = mkdtemp()
schema_file_path = os.path.join(schema_dir, "schema.json")

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


Expand All @@ -406,6 +431,7 @@ def main() -> None:
args.output,
tuple(args.exclude),
args.json2ts_cmd,
all_fields_required=args.all_fields_required,
)


Expand Down