@@ -599,6 +599,220 @@ def _extract_additional_imports(extra_template_data: defaultdict[str, dict[str,
599599# Types that are lost during JSON Schema conversion and need to be preserved
600600_PRESERVED_TYPE_ORIGINS : dict [type , str ] = {}
601601
602+ # Marker for types that Pydantic cannot serialize to JSON Schema
603+ _UNSERIALIZABLE_MARKER = "x-python-unserializable"
604+
605+
606+ def _serialize_python_type_full (tp : type ) -> str : # noqa: PLR0911
607+ """Serialize ANY Python type to its string representation.
608+
609+ Handles:
610+ - Basic types: str, int, bool, etc.
611+ - Generic types: List[str], Dict[str, int], etc.
612+ - Callable: Callable[[str], str], Callable[..., Any]
613+ - Union types: str | int, Optional[str]
614+ - Type: Type[BaseModel]
615+ - Custom classes: mymodule.MyClass
616+ - Nested generics: List[Callable[[str], str]]
617+ """
618+ import types # noqa: PLC0415
619+ from typing import Union , get_args , get_origin # noqa: PLC0415
620+
621+ if tp is type (None ): # pragma: no cover
622+ return "None"
623+
624+ if tp is ...: # pragma: no cover
625+ return "..."
626+
627+ origin = get_origin (tp )
628+ args = get_args (tp )
629+
630+ if origin is None :
631+ module = getattr (tp , "__module__" , "" )
632+ name = getattr (tp , "__name__" , None ) or getattr (tp , "__qualname__" , None )
633+
634+ if name is None :
635+ return str (tp ).replace ("typing." , "" )
636+
637+ if module and module not in {"builtins" , "typing" , "collections.abc" }:
638+ return f"{ module } .{ name } "
639+ return name
640+
641+ if _is_callable_origin (origin ):
642+ return _serialize_callable (args )
643+
644+ if origin is Union or (hasattr (types , "UnionType" ) and origin is types .UnionType ): # pragma: no cover
645+ parts = [_serialize_python_type_full (arg ) for arg in args ]
646+ return " | " .join (parts )
647+
648+ if origin is type :
649+ if args :
650+ return f"Type[{ _serialize_python_type_full (args [0 ])} ]"
651+ return "Type" # pragma: no cover
652+
653+ origin_name = _get_origin_name (origin )
654+ if args :
655+ args_str = ", " .join (_serialize_python_type_full (arg ) for arg in args )
656+ return f"{ origin_name } [{ args_str } ]"
657+
658+ return origin_name # pragma: no cover
659+
660+
661+ def _is_callable_origin (origin : type | None ) -> bool :
662+ """Check if origin is Callable."""
663+ if origin is None : # pragma: no cover
664+ return False
665+ from collections .abc import Callable as ABCCallable # noqa: PLC0415
666+
667+ if origin is ABCCallable :
668+ return True
669+ origin_str = str (origin )
670+ return "Callable" in origin_str or "callable" in origin_str
671+
672+
673+ def _serialize_callable (args : tuple [type , ...]) -> str :
674+ """Serialize Callable type."""
675+ if not args : # pragma: no cover
676+ return "Callable"
677+
678+ params = args [:- 1 ]
679+ ret = args [- 1 ]
680+
681+ if len (params ) == 1 and params [0 ] is ...:
682+ return f"Callable[..., { _serialize_python_type_full (ret )} ]"
683+
684+ if len (params ) == 1 and isinstance (params [0 ], (list , tuple )): # pragma: no cover
685+ params = tuple (params [0 ])
686+
687+ params_str = ", " .join (_serialize_python_type_full (p ) for p in params )
688+ return f"Callable[[{ params_str } ], { _serialize_python_type_full (ret )} ]"
689+
690+
691+ def _get_origin_name (origin : type ) -> str :
692+ """Get the name of a generic origin."""
693+ name = getattr (origin , "__name__" , None )
694+ if name :
695+ return name
696+
697+ # Fallback for origins without __name__ (rare edge case)
698+ origin_str = str (origin ) # pragma: no cover
699+ if "typing." in origin_str : # pragma: no cover
700+ return origin_str .replace ("typing." , "" )
701+
702+ return origin_str # pragma: no cover
703+
704+
705+ def _get_input_model_json_schema_class () -> type :
706+ """Get the InputModelJsonSchema class (lazy import to avoid Pydantic v1 issues)."""
707+ from pydantic .json_schema import GenerateJsonSchema # noqa: PLC0415
708+
709+ class InputModelJsonSchema (GenerateJsonSchema ):
710+ """Custom schema generator that handles ALL unserializable types."""
711+
712+ def handle_invalid_for_json_schema ( # noqa: PLR6301
713+ self ,
714+ schema : Any , # noqa: ARG002
715+ error_info : Any , # noqa: ARG002
716+ ) -> dict [str , Any ]:
717+ """Catch ALL types that Pydantic can't serialize to JSON Schema."""
718+ return {
719+ "type" : "object" ,
720+ _UNSERIALIZABLE_MARKER : True ,
721+ }
722+
723+ def callable_schema ( # noqa: PLR6301
724+ self ,
725+ schema : Any , # noqa: ARG002
726+ ) -> dict [str , Any ]:
727+ """Handle Callable types - these raise before handle_invalid_for_json_schema."""
728+ return {
729+ "type" : "string" ,
730+ _UNSERIALIZABLE_MARKER : True ,
731+ }
732+
733+ return InputModelJsonSchema
734+
735+
736+ def _is_type_origin (annotation : type ) -> bool :
737+ """Check if annotation is Type[X]."""
738+ from typing import get_origin # noqa: PLC0415
739+
740+ origin = get_origin (annotation )
741+ return origin is type
742+
743+
744+ def _process_unserializable_property (prop : dict [str , Any ], annotation : type ) -> None :
745+ """Process a single property, handling anyOf/oneOf/items structures."""
746+ if "anyOf" in prop :
747+ for item in prop ["anyOf" ]:
748+ if item .get (_UNSERIALIZABLE_MARKER ):
749+ _set_python_type_for_unserializable (item , annotation )
750+ elif "oneOf" in prop : # pragma: no cover
751+ for item in prop ["oneOf" ]:
752+ if item .get (_UNSERIALIZABLE_MARKER ):
753+ _set_python_type_for_unserializable (item , annotation )
754+ elif prop .get (_UNSERIALIZABLE_MARKER ):
755+ _set_python_type_for_unserializable (prop , annotation )
756+ elif "items" in prop and prop ["items" ].get (_UNSERIALIZABLE_MARKER ):
757+ prop ["x-python-type" ] = _serialize_python_type_full (annotation )
758+ prop ["items" ].pop (_UNSERIALIZABLE_MARKER , None )
759+ elif _is_type_origin (annotation ):
760+ prop ["x-python-type" ] = _serialize_python_type_full (annotation )
761+
762+
763+ def _set_python_type_for_unserializable (item : dict [str , Any ], annotation : type ) -> None :
764+ """Set x-python-type and clean up markers."""
765+ from typing import Union , get_args , get_origin # noqa: PLC0415
766+
767+ origin = get_origin (annotation )
768+ actual_type = annotation
769+
770+ if origin is Union :
771+ for arg in get_args (annotation ): # pragma: no branch
772+ if arg is not type (None ): # pragma: no branch
773+ actual_type = arg
774+ break
775+
776+ item ["x-python-type" ] = _serialize_python_type_full (actual_type )
777+ item .pop (_UNSERIALIZABLE_MARKER , None )
778+
779+
780+ def _add_python_type_for_unserializable (
781+ schema : dict [str , Any ],
782+ model : type ,
783+ visited_defs : set [str ] | None = None ,
784+ ) -> dict [str , Any ]:
785+ """Add x-python-type to ALL fields marked as unserializable.
786+
787+ Handles:
788+ - Top-level properties
789+ - Nested in anyOf/oneOf/allOf
790+ - $defs definitions
791+ """
792+ if visited_defs is None :
793+ visited_defs = set ()
794+
795+ if "properties" in schema :
796+ model_fields = getattr (model , "model_fields" , {})
797+ for field_name , prop in schema ["properties" ].items ():
798+ if field_name in model_fields : # pragma: no branch
799+ annotation = model_fields [field_name ].annotation
800+ _process_unserializable_property (prop , annotation )
801+
802+ if "$defs" in schema :
803+ nested_models = _collect_nested_models (model )
804+ model_name = getattr (model , "__name__" , None )
805+ if model_name : # pragma: no branch
806+ nested_models [model_name ] = model
807+ for def_name , def_schema in schema ["$defs" ].items ():
808+ if def_name in visited_defs : # pragma: no cover
809+ continue
810+ visited_defs .add (def_name )
811+ if def_name in nested_models : # pragma: no branch
812+ _add_python_type_for_unserializable (def_schema , nested_models [def_name ], visited_defs )
813+
814+ return schema
815+
602816
603817def _init_preserved_type_origins () -> dict [type , str ]:
604818 """Initialize preserved type origins mapping (lazy initialization)."""
@@ -937,7 +1151,9 @@ def _load_model_schema( # noqa: PLR0912, PLR0914, PLR0915
9371151 if not hasattr (obj , "model_json_schema" ):
9381152 msg = "--input-model with Pydantic model requires Pydantic v2 runtime. Please upgrade Pydantic to v2."
9391153 raise Error (msg )
940- schema = obj .model_json_schema ()
1154+ schema_generator = _get_input_model_json_schema_class ()
1155+ schema = obj .model_json_schema (schema_generator = schema_generator )
1156+ schema = _add_python_type_for_unserializable (schema , obj )
9411157 schema = _add_python_type_info (schema , obj )
9421158
9431159 if ref_strategy and ref_strategy != InputModelRefStrategy .RegenerateAll :
0 commit comments