11"""Json schema to dynamic pydantic model."""
22
3- from typing import Any , Dict , List , Optional , Type , Union
3+ from enum import Enum
4+ from typing import Any , Dict , List , Type , Union
45
56from pydantic import BaseModel , Field , create_model
67
78
89def jsonschema_to_pydantic (
910 schema : dict [str , Any ],
10- definitions : Optional [dict [str , Any ]] = None ,
1111) -> Type [BaseModel ]:
1212 """Convert a schema dict to a pydantic model.
1313
14- Modified version of https://github.com/kreneskyp/jsonschema-pydantic to account for two unresolved issues.
14+ Modified version of https://github.com/kreneskyp/jsonschema-pydantic to account for three unresolved issues.
15+ 1. Support for title
16+ 2. Better representation of optionals.
17+ 3. Support for optional
1518
1619 Args:
1720 schema: JSON schema.
1821 definitions: Definitions dict. Defaults to `$def`.
1922
2023 Returns: Pydantic model.
2124 """
22- title = schema .get ("title" , "DynamicModel" )
23- assert isinstance (title , str ), "Title of a model must be a string."
24-
25- description = schema .get ("description" , None )
26-
27- # top level schema provides definitions
28- if definitions is None :
29- if "$defs" in schema :
30- definitions = schema ["$defs" ]
31- elif "definitions" in schema :
32- definitions = schema ["definitions" ]
33- else :
34- definitions = {}
25+ dynamic_type_counter = 0
26+ combined_model_counter = 0
3527
3628 def convert_type (prop : dict [str , Any ]) -> Any :
29+ nonlocal dynamic_type_counter , combined_model_counter
3730 if "$ref" in prop :
38- ref_path = prop ["$ref" ].split ("/" )
39- ref = definitions [ref_path [- 1 ]]
40- return jsonschema_to_pydantic (ref , definitions )
31+ # This is the full path. It will be updated in update_forward_refs.
32+ return prop ["$ref" ].split ("/" )[- 1 ].capitalize ()
4133
4234 if "type" in prop :
4335 type_mapping = {
@@ -52,13 +44,55 @@ def convert_type(prop: dict[str, Any]) -> Any:
5244
5345 type_ = prop ["type" ]
5446
55- if type_ == "array" :
47+ if "enum" in prop :
48+ dynamic_members = {
49+ f"KEY_{ i } " : value for i , value in enumerate (prop ["enum" ])
50+ }
51+
52+ base_type : Any = type_mapping .get (type_ , Any )
53+
54+ class DynamicEnum (base_type , Enum ):
55+ pass
56+
57+ type_ = DynamicEnum (prop .get ("title" , "DynamicEnum" ), dynamic_members ) # type: ignore[call-arg]
58+ return type_
59+ elif type_ == "array" :
5660 item_type : Any = convert_type (prop .get ("items" , {}))
57- assert isinstance (item_type , type )
5861 return List [item_type ] # noqa F821
5962 elif type_ == "object" :
6063 if "properties" in prop :
61- return jsonschema_to_pydantic (prop , definitions )
64+ if "title" in prop and prop ["title" ]:
65+ title = prop ["title" ]
66+ else :
67+ title = f"DynamicType_{ dynamic_type_counter } "
68+ dynamic_type_counter += 1
69+
70+ fields : dict [str , Any ] = {}
71+ required_fields = prop .get ("required" , [])
72+
73+ for name , property in prop .get ("properties" , {}).items ():
74+ pydantic_type = convert_type (property )
75+ field_kwargs = {}
76+ if "default" in property :
77+ field_kwargs ["default" ] = property ["default" ]
78+ if name not in required_fields :
79+ # Note that we do not make this optional. This is due to a limitation in Pydantic/Python.
80+ # If we convert the Optional type back to json schema, it is represented as type | None.
81+ # pydantic_type = Optional[pydantic_type]
82+
83+ if "default" not in field_kwargs :
84+ field_kwargs ["default" ] = None
85+ if "description" in property :
86+ field_kwargs ["description" ] = property ["description" ]
87+ if "title" in property :
88+ field_kwargs ["title" ] = property ["title" ]
89+
90+ fields [name ] = (pydantic_type , Field (** field_kwargs ))
91+
92+ object_model = create_model (title , ** fields )
93+ if "description" in prop :
94+ object_model .__doc__ = prop ["description" ]
95+ return object_model
6296 else :
6397 return Dict [str , Any ]
6498 else :
@@ -67,9 +101,13 @@ def convert_type(prop: dict[str, Any]) -> Any:
67101 elif "allOf" in prop :
68102 combined_fields = {}
69103 for sub_schema in prop ["allOf" ]:
70- model = jsonschema_to_pydantic (sub_schema , definitions )
104+ model = convert_type (sub_schema )
71105 combined_fields .update (model .__annotations__ )
72- return create_model ("CombinedModel" , ** combined_fields )
106+ combined_model = create_model (
107+ f"CombinedModel_{ combined_model_counter } " , ** combined_fields
108+ )
109+ combined_model_counter += 1
110+ return combined_model
73111
74112 elif "anyOf" in prop :
75113 unioned_types = tuple (
@@ -81,31 +119,10 @@ def convert_type(prop: dict[str, Any]) -> Any:
81119 else :
82120 raise ValueError (f"Unsupported schema: { prop } " )
83121
84- fields : dict [str , Any ] = {}
85- required_fields = schema .get ("required" , [])
86-
87- for name , prop in schema .get ("properties" , {}).items ():
88- pydantic_type = convert_type (prop )
89- field_kwargs = {}
90- if "default" in prop :
91- field_kwargs ["default" ] = prop ["default" ]
92- if name not in required_fields :
93- # Note that we do not make this optional. This is due to a limitation in Pydantic/Python.
94- # If we convert the Optional type back to json schema, it is represented as type | None.
95- # pydantic_type = Optional[pydantic_type]
96-
97- if "default" not in field_kwargs :
98- field_kwargs ["default" ] = None
99- if "description" in prop :
100- field_kwargs ["description" ] = prop ["description" ]
101- if "title" in prop :
102- field_kwargs ["title" ] = prop ["title" ]
103-
104- fields [name ] = (pydantic_type , Field (** field_kwargs ))
105-
106- convert_type (schema .get ("properties" , {}).get ("choices" , {}))
107-
108- model = create_model (title , ** fields )
109- if description :
110- model .__doc__ = description
122+ namespace : dict [str , Any ] = {}
123+ for name , definition in schema .get ("$defs" , schema .get ("definitions" , {})).items ():
124+ model = convert_type (definition )
125+ namespace [name .capitalize ()] = model
126+ model = convert_type (schema )
127+ model .model_rebuild (force = True , _types_namespace = namespace )
111128 return model
0 commit comments