Skip to content
Merged
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