diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index d50154e18..abf886193 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -8,6 +8,7 @@ import enum as _enum import json +import re from collections import defaultdict from collections.abc import Iterable from contextlib import contextmanager, suppress @@ -1334,6 +1335,13 @@ def _is_compatible_python_type(self, schema_type: str | None, python_type: str) compatible = self.COMPATIBLE_PYTHON_TYPES.get(schema_type, frozenset()) return base_type in compatible + def _extract_all_type_names(self, type_str: str) -> list[str]: # noqa: PLR6301 + """Extract all type names from a type annotation string.""" + # Match type names: word characters starting with uppercase, not preceded by a dot + # This handles cases like Callable[[Iterable[str]], str] + pattern = r"(? DataType | None: """Get DataType from x-python-type if it's incompatible with schema type.""" x_python_type = obj.extras.get("x-python-type") @@ -1357,7 +1365,18 @@ def _get_python_type_override(self, obj: JsonSchemaObject) -> DataType | None: # If not in predefined imports, create import from the full path import_ = Import.from_full_path(prefix) - return self.data_type(type=type_str, import_=import_) + # Collect imports for all nested types (e.g., Iterable inside Callable[[Iterable[str]], str]) + nested_imports: list[DataType] = [] + for type_name in self._extract_all_type_names(type_str): + if type_name != base_type: + nested_import = self.PYTHON_TYPE_IMPORTS.get(type_name) + if nested_import: + nested_imports.append(self.data_type(import_=nested_import)) + + result = self.data_type(type=type_str, import_=import_) + if nested_imports: + result.data_types.extend(nested_imports) + return result def _apply_title_as_name(self, name: str, obj: JsonSchemaObject) -> str: """Apply title as name if use_title_as_name is enabled.""" diff --git a/tests/data/expected/main/jsonschema/x_python_type_nested_imports.py b/tests/data/expected/main/jsonschema/x_python_type_nested_imports.py new file mode 100644 index 000000000..7159eb358 --- /dev/null +++ b/tests/data/expected/main/jsonschema/x_python_type_nested_imports.py @@ -0,0 +1,14 @@ +# generated by datamodel-codegen: +# filename: x_python_type_nested_imports.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from typing import TypedDict + +from typing_extensions import NotRequired + + +class Model(TypedDict): + callback: NotRequired[Callable[[Iterable[str]], str]] diff --git a/tests/data/expected/main/jsonschema/x_python_type_nested_unknown_type.py b/tests/data/expected/main/jsonschema/x_python_type_nested_unknown_type.py new file mode 100644 index 000000000..7462eece5 --- /dev/null +++ b/tests/data/expected/main/jsonschema/x_python_type_nested_unknown_type.py @@ -0,0 +1,14 @@ +# generated by datamodel-codegen: +# filename: x_python_type_nested_unknown_type.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from collections.abc import Callable +from typing import TypedDict + +from typing_extensions import NotRequired + + +class Model(TypedDict): + callback: NotRequired[Callable[[MyCustomType[str]], str]] diff --git a/tests/data/jsonschema/x_python_type_nested_imports.json b/tests/data/jsonschema/x_python_type_nested_imports.json new file mode 100644 index 000000000..bb4dbd408 --- /dev/null +++ b/tests/data/jsonschema/x_python_type_nested_imports.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "callback": { + "type": "string", + "x-python-type": "Callable[[Iterable[str]], str]" + } + } +} diff --git a/tests/data/jsonschema/x_python_type_nested_unknown_type.json b/tests/data/jsonschema/x_python_type_nested_unknown_type.json new file mode 100644 index 000000000..c42b638cf --- /dev/null +++ b/tests/data/jsonschema/x_python_type_nested_unknown_type.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "callback": { + "type": "string", + "x-python-type": "Callable[[MyCustomType[str]], str]" + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 63f47286b..5cfcff7ad 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -7031,3 +7031,25 @@ def test_x_python_type_custom_fqpath(output_file: Path) -> None: assert_func=assert_file_content, extra_args=["--output-model-type", "typing.TypedDict"], ) + + +def test_x_python_type_nested_imports(output_file: Path) -> None: + """Test x-python-type with nested types that require imports (e.g., Callable[[Iterable[str]], str]).""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "x_python_type_nested_imports.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + extra_args=["--output-model-type", "typing.TypedDict"], + ) + + +def test_x_python_type_nested_unknown_type(output_file: Path) -> None: + """Test x-python-type with nested types not in PYTHON_TYPE_IMPORTS (e.g., MyCustomType).""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "x_python_type_nested_unknown_type.json", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + extra_args=["--output-model-type", "typing.TypedDict"], + )