datamodel-code-generator 0.11.12__py3-none-any.whl → 0.45.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. datamodel_code_generator/__init__.py +654 -185
  2. datamodel_code_generator/__main__.py +872 -388
  3. datamodel_code_generator/arguments.py +798 -0
  4. datamodel_code_generator/cli_options.py +295 -0
  5. datamodel_code_generator/format.py +292 -54
  6. datamodel_code_generator/http.py +85 -10
  7. datamodel_code_generator/imports.py +152 -43
  8. datamodel_code_generator/model/__init__.py +138 -1
  9. datamodel_code_generator/model/base.py +531 -120
  10. datamodel_code_generator/model/dataclass.py +211 -0
  11. datamodel_code_generator/model/enum.py +133 -12
  12. datamodel_code_generator/model/imports.py +22 -0
  13. datamodel_code_generator/model/msgspec.py +462 -0
  14. datamodel_code_generator/model/pydantic/__init__.py +30 -25
  15. datamodel_code_generator/model/pydantic/base_model.py +304 -100
  16. datamodel_code_generator/model/pydantic/custom_root_type.py +11 -2
  17. datamodel_code_generator/model/pydantic/dataclass.py +15 -4
  18. datamodel_code_generator/model/pydantic/imports.py +40 -27
  19. datamodel_code_generator/model/pydantic/types.py +188 -96
  20. datamodel_code_generator/model/pydantic_v2/__init__.py +51 -0
  21. datamodel_code_generator/model/pydantic_v2/base_model.py +268 -0
  22. datamodel_code_generator/model/pydantic_v2/imports.py +15 -0
  23. datamodel_code_generator/model/pydantic_v2/root_model.py +35 -0
  24. datamodel_code_generator/model/pydantic_v2/types.py +143 -0
  25. datamodel_code_generator/model/scalar.py +124 -0
  26. datamodel_code_generator/model/template/Enum.jinja2 +15 -2
  27. datamodel_code_generator/model/template/ScalarTypeAliasAnnotation.jinja2 +6 -0
  28. datamodel_code_generator/model/template/ScalarTypeAliasType.jinja2 +6 -0
  29. datamodel_code_generator/model/template/ScalarTypeStatement.jinja2 +6 -0
  30. datamodel_code_generator/model/template/TypeAliasAnnotation.jinja2 +20 -0
  31. datamodel_code_generator/model/template/TypeAliasType.jinja2 +20 -0
  32. datamodel_code_generator/model/template/TypeStatement.jinja2 +20 -0
  33. datamodel_code_generator/model/template/TypedDict.jinja2 +5 -0
  34. datamodel_code_generator/model/template/TypedDictClass.jinja2 +25 -0
  35. datamodel_code_generator/model/template/TypedDictFunction.jinja2 +24 -0
  36. datamodel_code_generator/model/template/UnionTypeAliasAnnotation.jinja2 +10 -0
  37. datamodel_code_generator/model/template/UnionTypeAliasType.jinja2 +10 -0
  38. datamodel_code_generator/model/template/UnionTypeStatement.jinja2 +10 -0
  39. datamodel_code_generator/model/template/dataclass.jinja2 +50 -0
  40. datamodel_code_generator/model/template/msgspec.jinja2 +55 -0
  41. datamodel_code_generator/model/template/pydantic/BaseModel.jinja2 +17 -4
  42. datamodel_code_generator/model/template/pydantic/BaseModel_root.jinja2 +12 -4
  43. datamodel_code_generator/model/template/pydantic/Config.jinja2 +1 -1
  44. datamodel_code_generator/model/template/pydantic/dataclass.jinja2 +15 -2
  45. datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 +57 -0
  46. datamodel_code_generator/model/template/pydantic_v2/ConfigDict.jinja2 +5 -0
  47. datamodel_code_generator/model/template/pydantic_v2/RootModel.jinja2 +48 -0
  48. datamodel_code_generator/model/type_alias.py +70 -0
  49. datamodel_code_generator/model/typed_dict.py +161 -0
  50. datamodel_code_generator/model/types.py +106 -0
  51. datamodel_code_generator/model/union.py +105 -0
  52. datamodel_code_generator/parser/__init__.py +30 -12
  53. datamodel_code_generator/parser/_graph.py +67 -0
  54. datamodel_code_generator/parser/_scc.py +171 -0
  55. datamodel_code_generator/parser/base.py +2426 -380
  56. datamodel_code_generator/parser/graphql.py +652 -0
  57. datamodel_code_generator/parser/jsonschema.py +2518 -647
  58. datamodel_code_generator/parser/openapi.py +631 -222
  59. datamodel_code_generator/py.typed +0 -0
  60. datamodel_code_generator/pydantic_patch.py +28 -0
  61. datamodel_code_generator/reference.py +672 -290
  62. datamodel_code_generator/types.py +521 -145
  63. datamodel_code_generator/util.py +155 -0
  64. datamodel_code_generator/watch.py +65 -0
  65. datamodel_code_generator-0.45.0.dist-info/METADATA +301 -0
  66. datamodel_code_generator-0.45.0.dist-info/RECORD +69 -0
  67. {datamodel_code_generator-0.11.12.dist-info → datamodel_code_generator-0.45.0.dist-info}/WHEEL +1 -1
  68. datamodel_code_generator-0.45.0.dist-info/entry_points.txt +2 -0
  69. datamodel_code_generator/version.py +0 -1
  70. datamodel_code_generator-0.11.12.dist-info/METADATA +0 -440
  71. datamodel_code_generator-0.11.12.dist-info/RECORD +0 -31
  72. datamodel_code_generator-0.11.12.dist-info/entry_points.txt +0 -3
  73. {datamodel_code_generator-0.11.12.dist-info → datamodel_code_generator-0.45.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,49 +1,65 @@
1
+ """OpenAPI and Swagger specification parser.
2
+
3
+ Extends JsonSchemaParser to handle OpenAPI 2.0 (Swagger), 3.0, and 3.1
4
+ specifications, including paths, operations, parameters, and request/response bodies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
1
9
  import re
2
10
  from collections import defaultdict
11
+ from contextlib import nullcontext
3
12
  from enum import Enum
4
13
  from pathlib import Path
5
- from typing import (
6
- Any,
7
- Callable,
8
- DefaultDict,
9
- Dict,
10
- Iterable,
11
- List,
12
- Mapping,
13
- Optional,
14
- Pattern,
15
- Sequence,
16
- Set,
17
- Tuple,
18
- Type,
19
- Union,
20
- )
21
- from urllib.parse import ParseResult
14
+ from re import Pattern
15
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, TypeVar, Union
16
+ from warnings import warn
22
17
 
23
- from pydantic import BaseModel, Field
18
+ from pydantic import Field
24
19
 
25
20
  from datamodel_code_generator import (
26
- DefaultPutDict,
21
+ DEFAULT_SHARED_MODULE_NAME,
22
+ AllOfMergeMode,
23
+ DataclassArguments,
24
+ Error,
27
25
  LiteralType,
28
26
  OpenAPIScope,
29
27
  PythonVersion,
30
- load_yaml,
28
+ PythonVersionMin,
29
+ ReadOnlyWriteOnlyModelType,
30
+ ReuseScope,
31
+ YamlValue,
32
+ load_yaml_dict,
31
33
  snooper_to_methods,
32
34
  )
35
+ from datamodel_code_generator.format import DEFAULT_FORMATTERS, DatetimeClassType, Formatter
33
36
  from datamodel_code_generator.model import DataModel, DataModelFieldBase
34
37
  from datamodel_code_generator.model import pydantic as pydantic_model
38
+ from datamodel_code_generator.parser.base import get_special_path
35
39
  from datamodel_code_generator.parser.jsonschema import (
36
40
  JsonSchemaObject,
37
41
  JsonSchemaParser,
38
42
  get_model_by_path,
39
- get_special_path,
40
43
  )
41
- from datamodel_code_generator.reference import snake_to_upper_camel
42
- from datamodel_code_generator.types import DataType, DataTypeManager, StrictTypes
44
+ from datamodel_code_generator.reference import FieldNameResolver, is_url, snake_to_upper_camel
45
+ from datamodel_code_generator.types import (
46
+ DataType,
47
+ DataTypeManager,
48
+ EmptyDataType,
49
+ StrictTypes,
50
+ )
51
+ from datamodel_code_generator.util import BaseModel
43
52
 
44
- RE_APPLICATION_JSON_PATTERN: Pattern[str] = re.compile(r'^application/.*json$')
53
+ if TYPE_CHECKING:
54
+ from collections.abc import Iterable, Mapping, Sequence
55
+ from urllib.parse import ParseResult
45
56
 
46
- OPERATION_NAMES: List[str] = [
57
+ from datamodel_code_generator.parser import DefaultPutDict
58
+
59
+
60
+ RE_APPLICATION_JSON_PATTERN: Pattern[str] = re.compile(r"^application/.*json$")
61
+
62
+ OPERATION_NAMES: list[str] = [
47
63
  "get",
48
64
  "put",
49
65
  "post",
@@ -56,131 +72,209 @@ OPERATION_NAMES: List[str] = [
56
72
 
57
73
 
58
74
  class ParameterLocation(Enum):
59
- query = 'query'
60
- header = 'header'
61
- path = 'path'
62
- cookie = 'cookie'
75
+ """Represent OpenAPI parameter locations."""
76
+
77
+ query = "query"
78
+ header = "header"
79
+ path = "path"
80
+ cookie = "cookie"
81
+
82
+
83
+ BaseModelT = TypeVar("BaseModelT", bound=BaseModel)
63
84
 
64
85
 
65
86
  class ReferenceObject(BaseModel):
66
- ref: str = Field(..., alias='$ref')
87
+ """Represent an OpenAPI reference object ($ref)."""
88
+
89
+ ref: str = Field(..., alias="$ref")
67
90
 
68
91
 
69
92
  class ExampleObject(BaseModel):
70
- summary: Optional[str]
71
- description: Optional[str]
72
- value: Any
73
- externalValue: Optional[str]
93
+ """Represent an OpenAPI example object."""
94
+
95
+ summary: Optional[str] = None # noqa: UP045
96
+ description: Optional[str] = None # noqa: UP045
97
+ value: YamlValue = None
98
+ externalValue: Optional[str] = None # noqa: N815, UP045
74
99
 
75
100
 
76
101
  class MediaObject(BaseModel):
77
- schema_: Union[ReferenceObject, JsonSchemaObject, None] = Field(
78
- None, alias='schema'
79
- )
80
- example: Any
81
- examples: Union[str, ReferenceObject, ExampleObject, None]
102
+ """Represent an OpenAPI media type object."""
103
+
104
+ schema_: Optional[Union[ReferenceObject, JsonSchemaObject]] = Field(None, alias="schema") # noqa: UP007, UP045
105
+ example: YamlValue = None
106
+ examples: Optional[Union[str, ReferenceObject, ExampleObject]] = None # noqa: UP007, UP045
82
107
 
83
108
 
84
109
  class ParameterObject(BaseModel):
85
- name: Optional[str]
86
- in_: Optional[ParameterLocation] = Field(None, alias='in')
87
- description: Optional[str]
110
+ """Represent an OpenAPI parameter object."""
111
+
112
+ name: Optional[str] = None # noqa: UP045
113
+ in_: Optional[ParameterLocation] = Field(None, alias="in") # noqa: UP045
114
+ description: Optional[str] = None # noqa: UP045
88
115
  required: bool = False
89
116
  deprecated: bool = False
90
- schema_: Optional[JsonSchemaObject] = Field(None, alias='schema')
91
- example: Any
92
- examples: Union[str, ReferenceObject, ExampleObject, None]
93
- content: Dict[str, MediaObject] = {}
117
+ schema_: Optional[JsonSchemaObject] = Field(None, alias="schema") # noqa: UP045
118
+ example: YamlValue = None
119
+ examples: Optional[Union[str, ReferenceObject, ExampleObject]] = None # noqa: UP007, UP045
120
+ content: dict[str, MediaObject] = {} # noqa: RUF012
94
121
 
95
122
 
96
123
  class HeaderObject(BaseModel):
97
- description: Optional[str]
124
+ """Represent an OpenAPI header object."""
125
+
126
+ description: Optional[str] = None # noqa: UP045
98
127
  required: bool = False
99
128
  deprecated: bool = False
100
- schema_: Optional[JsonSchemaObject] = Field(None, alias='schema')
101
- example: Any
102
- examples: Union[str, ReferenceObject, ExampleObject, None]
103
- content: Dict[str, MediaObject] = {}
129
+ schema_: Optional[JsonSchemaObject] = Field(None, alias="schema") # noqa: UP045
130
+ example: YamlValue = None
131
+ examples: Optional[Union[str, ReferenceObject, ExampleObject]] = None # noqa: UP007, UP045
132
+ content: dict[str, MediaObject] = {} # noqa: RUF012
104
133
 
105
134
 
106
135
  class RequestBodyObject(BaseModel):
107
- description: Optional[str]
108
- content: Dict[str, MediaObject] = {}
136
+ """Represent an OpenAPI request body object."""
137
+
138
+ description: Optional[str] = None # noqa: UP045
139
+ content: dict[str, MediaObject] = {} # noqa: RUF012
109
140
  required: bool = False
110
141
 
111
142
 
112
143
  class ResponseObject(BaseModel):
113
- description: Optional[str]
114
- headers: Dict[str, ParameterObject] = {}
115
- content: Dict[str, MediaObject] = {}
144
+ """Represent an OpenAPI response object."""
145
+
146
+ description: Optional[str] = None # noqa: UP045
147
+ headers: dict[str, ParameterObject] = {} # noqa: RUF012
148
+ content: dict[Union[str, int], MediaObject] = {} # noqa: RUF012, UP007
116
149
 
117
150
 
118
151
  class Operation(BaseModel):
119
- tags: List[str] = []
120
- summary: Optional[str]
121
- description: Optional[str]
122
- operationId: Optional[str]
123
- parameters: List[Union[ReferenceObject, ParameterObject]] = []
124
- requestBody: Union[ReferenceObject, RequestBodyObject, None]
125
- responses: Dict[str, Union[ReferenceObject, ResponseObject]] = {}
152
+ """Represent an OpenAPI operation object."""
153
+
154
+ tags: list[str] = [] # noqa: RUF012
155
+ summary: Optional[str] = None # noqa: UP045
156
+ description: Optional[str] = None # noqa: UP045
157
+ operationId: Optional[str] = None # noqa: N815, UP045
158
+ parameters: list[Union[ReferenceObject, ParameterObject]] = [] # noqa: RUF012, UP007
159
+ requestBody: Optional[Union[ReferenceObject, RequestBodyObject]] = None # noqa: N815, UP007, UP045
160
+ responses: dict[Union[str, int], Union[ReferenceObject, ResponseObject]] = {} # noqa: RUF012, UP007
126
161
  deprecated: bool = False
127
162
 
128
163
 
129
164
  class ComponentsObject(BaseModel):
130
- schemas: Dict[str, Union[ReferenceObject, JsonSchemaObject]] = {}
131
- responses: Dict[str, Union[ReferenceObject, ResponseObject]] = {}
132
- examples: Dict[str, Union[ReferenceObject, ExampleObject]] = {}
133
- requestBodies: Dict[str, Union[ReferenceObject, RequestBodyObject]] = {}
134
- headers: Dict[str, Union[ReferenceObject, HeaderObject]] = {}
165
+ """Represent an OpenAPI components object."""
135
166
 
167
+ schemas: dict[str, Union[ReferenceObject, JsonSchemaObject]] = {} # noqa: RUF012, UP007
168
+ responses: dict[str, Union[ReferenceObject, ResponseObject]] = {} # noqa: RUF012, UP007
169
+ examples: dict[str, Union[ReferenceObject, ExampleObject]] = {} # noqa: RUF012, UP007
170
+ requestBodies: dict[str, Union[ReferenceObject, RequestBodyObject]] = {} # noqa: N815, RUF012, UP007
171
+ headers: dict[str, Union[ReferenceObject, HeaderObject]] = {} # noqa: RUF012, UP007
136
172
 
137
- @snooper_to_methods(max_variable_length=None)
173
+
174
+ @snooper_to_methods()
138
175
  class OpenAPIParser(JsonSchemaParser):
139
- def __init__(
176
+ """Parser for OpenAPI 2.0/3.0/3.1 and Swagger specifications."""
177
+
178
+ SCHEMA_PATHS: ClassVar[list[str]] = ["#/components/schemas"]
179
+
180
+ def __init__( # noqa: PLR0913
140
181
  self,
141
- source: Union[str, Path, List[Path], ParseResult],
182
+ source: str | Path | list[Path] | ParseResult,
142
183
  *,
143
- data_model_type: Type[DataModel] = pydantic_model.BaseModel,
144
- data_model_root_type: Type[DataModel] = pydantic_model.CustomRootType,
145
- data_type_manager_type: Type[DataTypeManager] = pydantic_model.DataTypeManager,
146
- data_model_field_type: Type[DataModelFieldBase] = pydantic_model.DataModelField,
147
- base_class: Optional[str] = None,
148
- custom_template_dir: Optional[Path] = None,
149
- extra_template_data: Optional[DefaultDict[str, Dict[str, Any]]] = None,
150
- target_python_version: PythonVersion = PythonVersion.PY_37,
151
- dump_resolve_reference_action: Optional[Callable[[Iterable[str]], str]] = None,
184
+ data_model_type: type[DataModel] = pydantic_model.BaseModel,
185
+ data_model_root_type: type[DataModel] = pydantic_model.CustomRootType,
186
+ data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager,
187
+ data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField,
188
+ base_class: str | None = None,
189
+ additional_imports: list[str] | None = None,
190
+ custom_template_dir: Path | None = None,
191
+ extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
192
+ target_python_version: PythonVersion = PythonVersionMin,
193
+ dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = None,
152
194
  validation: bool = False,
153
195
  field_constraints: bool = False,
154
196
  snake_case_field: bool = False,
155
197
  strip_default_none: bool = False,
156
- aliases: Optional[Mapping[str, str]] = None,
198
+ aliases: Mapping[str, str] | None = None,
157
199
  allow_population_by_field_name: bool = False,
200
+ allow_extra_fields: bool = False,
201
+ extra_fields: str | None = None,
158
202
  apply_default_values_for_required_fields: bool = False,
159
203
  force_optional_for_required_fields: bool = False,
160
- class_name: Optional[str] = None,
204
+ class_name: str | None = None,
161
205
  use_standard_collections: bool = False,
162
- base_path: Optional[Path] = None,
206
+ base_path: Path | None = None,
163
207
  use_schema_description: bool = False,
208
+ use_field_description: bool = False,
209
+ use_attribute_docstrings: bool = False,
210
+ use_inline_field_description: bool = False,
211
+ use_default_kwarg: bool = False,
164
212
  reuse_model: bool = False,
165
- encoding: str = 'utf-8',
166
- enum_field_as_literal: Optional[LiteralType] = None,
213
+ reuse_scope: ReuseScope | None = None,
214
+ shared_module_name: str = DEFAULT_SHARED_MODULE_NAME,
215
+ encoding: str = "utf-8",
216
+ enum_field_as_literal: LiteralType | None = None,
217
+ use_one_literal_as_default: bool = False,
218
+ use_enum_values_in_discriminator: bool = False,
167
219
  set_default_enum_member: bool = False,
220
+ use_subclass_enum: bool = False,
221
+ use_specialized_enum: bool = True,
168
222
  strict_nullable: bool = False,
169
223
  use_generic_container_types: bool = False,
170
224
  enable_faux_immutability: bool = False,
171
- remote_text_cache: Optional[DefaultPutDict[str, str]] = None,
225
+ remote_text_cache: DefaultPutDict[str, str] | None = None,
172
226
  disable_appending_item_suffix: bool = False,
173
- strict_types: Optional[Sequence[StrictTypes]] = None,
174
- empty_enum_field_name: Optional[str] = None,
175
- custom_class_name_generator: Optional[Callable[[str], str]] = None,
176
- field_extra_keys: Optional[Set[str]] = None,
227
+ strict_types: Sequence[StrictTypes] | None = None,
228
+ empty_enum_field_name: str | None = None,
229
+ custom_class_name_generator: Callable[[str], str] | None = None,
230
+ field_extra_keys: set[str] | None = None,
177
231
  field_include_all_keys: bool = False,
178
- openapi_scopes: Optional[List[OpenAPIScope]] = None,
179
- wrap_string_literal: Optional[bool] = False,
232
+ field_extra_keys_without_x_prefix: set[str] | None = None,
233
+ openapi_scopes: list[OpenAPIScope] | None = None,
234
+ include_path_parameters: bool = False,
235
+ wrap_string_literal: bool | None = False,
180
236
  use_title_as_name: bool = False,
181
- http_headers: Optional[Sequence[Tuple[str, str]]] = None,
237
+ use_operation_id_as_name: bool = False,
238
+ use_unique_items_as_set: bool = False,
239
+ allof_merge_mode: AllOfMergeMode = AllOfMergeMode.Constraints,
240
+ http_headers: Sequence[tuple[str, str]] | None = None,
241
+ http_ignore_tls: bool = False,
182
242
  use_annotated: bool = False,
183
- ):
243
+ use_serialize_as_any: bool = False,
244
+ use_non_positive_negative_number_constrained_types: bool = False,
245
+ use_decimal_for_multiple_of: bool = False,
246
+ original_field_name_delimiter: str | None = None,
247
+ use_double_quotes: bool = False,
248
+ use_union_operator: bool = False,
249
+ allow_responses_without_content: bool = False,
250
+ collapse_root_models: bool = False,
251
+ skip_root_model: bool = False,
252
+ use_type_alias: bool = False,
253
+ special_field_name_prefix: str | None = None,
254
+ remove_special_field_name_prefix: bool = False,
255
+ capitalise_enum_members: bool = False,
256
+ keep_model_order: bool = False,
257
+ known_third_party: list[str] | None = None,
258
+ custom_formatters: list[str] | None = None,
259
+ custom_formatters_kwargs: dict[str, Any] | None = None,
260
+ use_pendulum: bool = False,
261
+ http_query_parameters: Sequence[tuple[str, str]] | None = None,
262
+ treat_dot_as_module: bool = False,
263
+ use_exact_imports: bool = False,
264
+ default_field_extras: dict[str, Any] | None = None,
265
+ target_datetime_class: DatetimeClassType | None = None,
266
+ keyword_only: bool = False,
267
+ frozen_dataclasses: bool = False,
268
+ no_alias: bool = False,
269
+ formatters: list[Formatter] = DEFAULT_FORMATTERS,
270
+ parent_scoped_naming: bool = False,
271
+ dataclass_arguments: DataclassArguments | None = None,
272
+ type_mappings: list[str] | None = None,
273
+ read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None,
274
+ use_frozen_field: bool = False,
275
+ ) -> None:
276
+ """Initialize the OpenAPI parser with extensive configuration options."""
277
+ target_datetime_class = target_datetime_class or DatetimeClassType.Awaredatetime
184
278
  super().__init__(
185
279
  source=source,
186
280
  data_model_type=data_model_type,
@@ -188,6 +282,7 @@ class OpenAPIParser(JsonSchemaParser):
188
282
  data_type_manager_type=data_type_manager_type,
189
283
  data_model_field_type=data_model_field_type,
190
284
  base_class=base_class,
285
+ additional_imports=additional_imports,
191
286
  custom_template_dir=custom_template_dir,
192
287
  extra_template_data=extra_template_data,
193
288
  target_python_version=target_python_version,
@@ -198,16 +293,28 @@ class OpenAPIParser(JsonSchemaParser):
198
293
  strip_default_none=strip_default_none,
199
294
  aliases=aliases,
200
295
  allow_population_by_field_name=allow_population_by_field_name,
296
+ allow_extra_fields=allow_extra_fields,
297
+ extra_fields=extra_fields,
201
298
  apply_default_values_for_required_fields=apply_default_values_for_required_fields,
202
299
  force_optional_for_required_fields=force_optional_for_required_fields,
203
300
  class_name=class_name,
204
301
  use_standard_collections=use_standard_collections,
205
302
  base_path=base_path,
206
303
  use_schema_description=use_schema_description,
304
+ use_field_description=use_field_description,
305
+ use_attribute_docstrings=use_attribute_docstrings,
306
+ use_inline_field_description=use_inline_field_description,
307
+ use_default_kwarg=use_default_kwarg,
207
308
  reuse_model=reuse_model,
309
+ reuse_scope=reuse_scope,
310
+ shared_module_name=shared_module_name,
208
311
  encoding=encoding,
209
312
  enum_field_as_literal=enum_field_as_literal,
313
+ use_one_literal_as_default=use_one_literal_as_default,
314
+ use_enum_values_in_discriminator=use_enum_values_in_discriminator,
210
315
  set_default_enum_member=set_default_enum_member,
316
+ use_subclass_enum=use_subclass_enum,
317
+ use_specialized_enum=use_specialized_enum,
211
318
  strict_nullable=strict_nullable,
212
319
  use_generic_container_types=use_generic_container_types,
213
320
  enable_faux_immutability=enable_faux_immutability,
@@ -218,50 +325,197 @@ class OpenAPIParser(JsonSchemaParser):
218
325
  custom_class_name_generator=custom_class_name_generator,
219
326
  field_extra_keys=field_extra_keys,
220
327
  field_include_all_keys=field_include_all_keys,
328
+ field_extra_keys_without_x_prefix=field_extra_keys_without_x_prefix,
221
329
  wrap_string_literal=wrap_string_literal,
222
330
  use_title_as_name=use_title_as_name,
331
+ use_operation_id_as_name=use_operation_id_as_name,
332
+ use_unique_items_as_set=use_unique_items_as_set,
333
+ allof_merge_mode=allof_merge_mode,
223
334
  http_headers=http_headers,
335
+ http_ignore_tls=http_ignore_tls,
224
336
  use_annotated=use_annotated,
337
+ use_serialize_as_any=use_serialize_as_any,
338
+ use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types,
339
+ use_decimal_for_multiple_of=use_decimal_for_multiple_of,
340
+ original_field_name_delimiter=original_field_name_delimiter,
341
+ use_double_quotes=use_double_quotes,
342
+ use_union_operator=use_union_operator,
343
+ allow_responses_without_content=allow_responses_without_content,
344
+ collapse_root_models=collapse_root_models,
345
+ skip_root_model=skip_root_model,
346
+ use_type_alias=use_type_alias,
347
+ special_field_name_prefix=special_field_name_prefix,
348
+ remove_special_field_name_prefix=remove_special_field_name_prefix,
349
+ capitalise_enum_members=capitalise_enum_members,
350
+ keep_model_order=keep_model_order,
351
+ known_third_party=known_third_party,
352
+ custom_formatters=custom_formatters,
353
+ custom_formatters_kwargs=custom_formatters_kwargs,
354
+ use_pendulum=use_pendulum,
355
+ http_query_parameters=http_query_parameters,
356
+ treat_dot_as_module=treat_dot_as_module,
357
+ use_exact_imports=use_exact_imports,
358
+ default_field_extras=default_field_extras,
359
+ target_datetime_class=target_datetime_class,
360
+ keyword_only=keyword_only,
361
+ frozen_dataclasses=frozen_dataclasses,
362
+ no_alias=no_alias,
363
+ formatters=formatters,
364
+ parent_scoped_naming=parent_scoped_naming,
365
+ dataclass_arguments=dataclass_arguments,
366
+ type_mappings=type_mappings,
367
+ read_only_write_only_model_type=read_only_write_only_model_type,
368
+ use_frozen_field=use_frozen_field,
225
369
  )
226
- self.open_api_scopes: List[OpenAPIScope] = openapi_scopes or [
227
- OpenAPIScope.Schemas
228
- ]
229
-
230
- def get_ref_model(self, ref: str) -> Dict[str, Any]:
231
- ref_file, ref_path = self.model_resolver.resolve_ref(ref).split('#', 1)
232
- if ref_file:
233
- ref_body = self._get_ref_body(ref_file)
234
- else: # pragma: no cover
235
- ref_body = self.raw_obj
236
- return get_model_by_path(ref_body, ref_path.split('/')[1:])
237
-
238
- def parse_parameters(self, parameters: ParameterObject, path: List[str]) -> None:
239
- if parameters.name and parameters.schema_: # pragma: no cover
240
- self.parse_item(parameters.name, parameters.schema_, [*path, 'schema'])
241
- for (
242
- media_type,
243
- media_obj,
244
- ) in parameters.content.items(): # type: str, MediaObject
245
- if parameters.name and isinstance( # pragma: no cover
246
- media_obj.schema_, JsonSchemaObject
247
- ): # pragma: no cover
248
- self.parse_item(parameters.name, media_obj.schema_, [*path, media_type])
370
+ self.open_api_scopes: list[OpenAPIScope] = openapi_scopes or [OpenAPIScope.Schemas]
371
+ self.include_path_parameters: bool = include_path_parameters
372
+ self._discriminator_schemas: dict[str, dict[str, Any]] = {}
373
+ self._discriminator_subtypes: dict[str, list[str]] = defaultdict(list)
374
+
375
+ def get_ref_model(self, ref: str) -> dict[str, Any]:
376
+ """Resolve a reference to its model definition."""
377
+ ref_file, ref_path = self.model_resolver.resolve_ref(ref).split("#", 1)
378
+ ref_body = self._get_ref_body(ref_file) if ref_file else self.raw_obj
379
+ return get_model_by_path(ref_body, ref_path.split("/")[1:])
380
+
381
+ def get_data_type(self, obj: JsonSchemaObject) -> DataType:
382
+ """Get data type from JSON schema object, handling OpenAPI nullable semantics."""
383
+ # OpenAPI 3.0 doesn't allow `null` in the `type` field and list of types
384
+ # https://swagger.io/docs/specification/data-models/data-types/#null
385
+ # OpenAPI 3.1 does allow `null` in the `type` field and is equivalent to
386
+ # a `nullable` flag on the property itself
387
+ if obj.nullable and self.strict_nullable and isinstance(obj.type, str):
388
+ obj.type = [obj.type, "null"]
389
+
390
+ return super().get_data_type(obj)
391
+
392
+ def _get_discriminator_union_type(self, ref: str) -> DataType | None:
393
+ """Create a union type for discriminator subtypes if available."""
394
+ subtypes = self._discriminator_subtypes.get(ref, [])
395
+ if not subtypes:
396
+ return None
397
+ refs = map(self.model_resolver.add_ref, subtypes)
398
+ return self.data_type(data_types=[self.data_type(reference=r) for r in refs])
399
+
400
+ def get_ref_data_type(self, ref: str) -> DataType:
401
+ """Get data type for a reference, handling discriminator polymorphism."""
402
+ if ref in self._discriminator_schemas and (union_type := self._get_discriminator_union_type(ref)):
403
+ return union_type
404
+ return super().get_ref_data_type(ref)
405
+
406
+ def parse_object_fields(
407
+ self,
408
+ obj: JsonSchemaObject,
409
+ path: list[str],
410
+ module_name: Optional[str] = None, # noqa: UP045
411
+ class_name: Optional[str] = None, # noqa: UP045
412
+ ) -> list[DataModelFieldBase]:
413
+ """Parse object fields, adding discriminator info for allOf polymorphism."""
414
+ fields = super().parse_object_fields(obj, path, module_name, class_name=class_name)
415
+ properties = obj.properties or {}
416
+
417
+ result_fields: list[DataModelFieldBase] = []
418
+ for field_obj in fields:
419
+ field = properties.get(field_obj.original_name)
420
+
421
+ if (
422
+ isinstance(field, JsonSchemaObject)
423
+ and field.ref
424
+ and (discriminator := self._discriminator_schemas.get(field.ref))
425
+ ):
426
+ new_field_type = self._get_discriminator_union_type(field.ref) or field_obj.data_type
427
+ field_obj = self.data_model_field_type(**{ # noqa: PLW2901
428
+ **field_obj.__dict__,
429
+ "data_type": new_field_type,
430
+ "extras": {**field_obj.extras, "discriminator": discriminator},
431
+ })
432
+ result_fields.append(field_obj)
433
+
434
+ return result_fields
435
+
436
+ def resolve_object(self, obj: ReferenceObject | BaseModelT, object_type: type[BaseModelT]) -> BaseModelT:
437
+ """Resolve a reference object to its actual type or return the object as-is."""
438
+ if isinstance(obj, ReferenceObject):
439
+ ref_obj = self.get_ref_model(obj.ref)
440
+ return object_type.parse_obj(ref_obj)
441
+ return obj
442
+
443
+ def _parse_schema_or_ref(
444
+ self,
445
+ name: str,
446
+ schema: JsonSchemaObject | ReferenceObject | None,
447
+ path: list[str],
448
+ ) -> DataType | None:
449
+ """Parse a schema object or resolve a reference to get DataType."""
450
+ if schema is None:
451
+ return None
452
+ if isinstance(schema, JsonSchemaObject):
453
+ return self.parse_schema(name, schema, path)
454
+ self.resolve_ref(schema.ref)
455
+ return self.get_ref_data_type(schema.ref)
456
+
457
+ def _process_path_items( # noqa: PLR0913
458
+ self,
459
+ items: dict[str, dict[str, Any]],
460
+ base_path: list[str],
461
+ scope_name: str,
462
+ global_parameters: list[dict[str, Any]],
463
+ security: list[dict[str, list[str]]] | None,
464
+ *,
465
+ strip_leading_slash: bool = True,
466
+ ) -> None:
467
+ """Process path or webhook items with operations."""
468
+ scope_path = [*base_path, f"#/{scope_name}"]
469
+ for item_name, methods_ in items.items():
470
+ item_ref = methods_.get("$ref")
471
+ if item_ref:
472
+ methods = self.get_ref_model(item_ref)
473
+ # Extract base path from reference for external file resolution
474
+ resolved_ref = self.model_resolver.resolve_ref(item_ref)
475
+ ref_file = resolved_ref.split("#")[0] if "#" in resolved_ref else resolved_ref
476
+ ref_base_path = Path(ref_file).parent if ref_file and not is_url(ref_file) else None
477
+ else:
478
+ methods = methods_
479
+ ref_base_path = None
480
+
481
+ item_parameters = global_parameters.copy()
482
+ if "parameters" in methods:
483
+ item_parameters.extend(methods["parameters"])
484
+
485
+ relative_name = item_name[1:] if strip_leading_slash else item_name.removeprefix("/")
486
+ path = [*scope_path, relative_name] if relative_name else get_special_path("root", scope_path)
487
+
488
+ base_path_context = (
489
+ self.model_resolver.current_base_path_context(ref_base_path) if ref_base_path else nullcontext()
490
+ )
491
+ with base_path_context:
492
+ for operation_name, raw_operation in methods.items():
493
+ if operation_name not in OPERATION_NAMES:
494
+ continue
495
+ if item_parameters:
496
+ if "parameters" in raw_operation:
497
+ raw_operation["parameters"].extend(item_parameters)
498
+ else:
499
+ raw_operation["parameters"] = item_parameters.copy()
500
+ if security is not None and "security" not in raw_operation:
501
+ raw_operation["security"] = security
502
+ self.parse_operation(raw_operation, [*path, operation_name])
249
503
 
250
504
  def parse_schema(
251
505
  self,
252
506
  name: str,
253
507
  obj: JsonSchemaObject,
254
- path: List[str],
508
+ path: list[str],
255
509
  ) -> DataType:
510
+ """Parse a JSON schema object into a data type."""
256
511
  if obj.is_array:
257
- data_type: DataType = self.parse_array_fields(
258
- name, obj, [*path, name], False
259
- ).data_type
260
- # TODO: The List model is not created by this method. Some scenarios may necessitate it.
512
+ data_type = self.parse_array(name, obj, [*path, name])
261
513
  elif obj.allOf: # pragma: no cover
262
514
  data_type = self.parse_all_of(name, obj, path)
263
- elif obj.oneOf: # pragma: no cover
515
+ elif obj.oneOf or obj.anyOf: # pragma: no cover
264
516
  data_type = self.parse_root_type(name, obj, path)
517
+ if isinstance(data_type, EmptyDataType) and obj.properties:
518
+ self.parse_object(name, obj, path)
265
519
  elif obj.is_object:
266
520
  data_type = self.parse_object(name, obj, path)
267
521
  elif obj.enum: # pragma: no cover
@@ -277,66 +531,197 @@ class OpenAPIParser(JsonSchemaParser):
277
531
  self,
278
532
  name: str,
279
533
  request_body: RequestBodyObject,
280
- path: List[str],
281
- ) -> None:
282
- for (
283
- media_type,
284
- media_obj,
285
- ) in request_body.content.items(): # type: str, MediaObject
286
- if isinstance(media_obj.schema_, JsonSchemaObject):
287
- self.parse_schema(name, media_obj.schema_, [*path, media_type])
534
+ path: list[str],
535
+ ) -> dict[str, DataType]:
536
+ """Parse request body content into data types by media type."""
537
+ data_types: dict[str, DataType] = {}
538
+ for media_type, media_obj in request_body.content.items():
539
+ data_type = self._parse_schema_or_ref(name, media_obj.schema_, [*path, media_type])
540
+ if data_type:
541
+ data_types[media_type] = data_type
542
+ return data_types
288
543
 
289
544
  def parse_responses(
290
545
  self,
291
546
  name: str,
292
- responses: Dict[str, Union[ReferenceObject, ResponseObject]],
293
- path: List[str],
294
- ) -> Dict[str, Dict[str, DataType]]:
295
- data_types: DefaultDict[str, Dict[str, DataType]] = defaultdict(dict)
547
+ responses: dict[str | int, ReferenceObject | ResponseObject],
548
+ path: list[str],
549
+ ) -> dict[str | int, dict[str, DataType]]:
550
+ """Parse response objects into data types by status code and content type."""
551
+ data_types: defaultdict[str | int, dict[str, DataType]] = defaultdict(dict)
296
552
  for status_code, detail in responses.items():
297
553
  if isinstance(detail, ReferenceObject):
298
554
  if not detail.ref: # pragma: no cover
299
555
  continue
300
556
  ref_model = self.get_ref_model(detail.ref)
301
- content = {
302
- k: MediaObject.parse_obj(v)
303
- for k, v in ref_model.get("content", {}).items()
304
- }
557
+ content = {k: MediaObject.parse_obj(v) for k, v in ref_model.get("content", {}).items()}
305
558
  else:
306
559
  content = detail.content
560
+
561
+ if self.allow_responses_without_content and not content:
562
+ data_types[status_code]["application/json"] = DataType(type="None")
563
+
307
564
  for content_type, obj in content.items():
565
+ response_path: list[str] = [*path, str(status_code), str(content_type)]
566
+ data_type = self._parse_schema_or_ref(name, obj.schema_, response_path)
567
+ if data_type:
568
+ data_types[status_code][content_type] = data_type # pyright: ignore[reportArgumentType]
308
569
 
309
- object_schema = obj.schema_
310
- if not object_schema: # pragma: no cover
311
- continue
312
- if isinstance(object_schema, JsonSchemaObject):
313
- data_types[status_code][content_type] = self.parse_schema(
314
- name, object_schema, [*path, status_code, content_type]
570
+ return data_types
571
+
572
+ @classmethod
573
+ def parse_tags(
574
+ cls,
575
+ name: str, # noqa: ARG003
576
+ tags: list[str],
577
+ path: list[str], # noqa: ARG003
578
+ ) -> list[str]:
579
+ """Parse operation tags."""
580
+ return tags
581
+
582
+ _field_name_resolver: FieldNameResolver = FieldNameResolver()
583
+
584
+ @classmethod
585
+ def _get_model_name(cls, path_name: str, method: str, suffix: str) -> str:
586
+ normalized = cls._field_name_resolver.get_valid_name(path_name, ignore_snake_case_field=True)
587
+ camel_path_name = snake_to_upper_camel(normalized)
588
+ return f"{camel_path_name}{method.capitalize()}{suffix}"
589
+
590
+ def parse_all_parameters(
591
+ self,
592
+ name: str,
593
+ parameters: list[ReferenceObject | ParameterObject],
594
+ path: list[str],
595
+ ) -> DataType | None:
596
+ """Parse all operation parameters into a data model."""
597
+ fields: list[DataModelFieldBase] = []
598
+ exclude_field_names: set[str] = set()
599
+ reference = self.model_resolver.add(path, name, class_name=True, unique=True)
600
+ for parameter_ in parameters:
601
+ parameter = self.resolve_object(parameter_, ParameterObject)
602
+ parameter_name = parameter.name
603
+ if (
604
+ not parameter_name
605
+ or parameter.in_ not in {ParameterLocation.query, ParameterLocation.path}
606
+ or (parameter.in_ == ParameterLocation.path and not self.include_path_parameters)
607
+ ):
608
+ continue
609
+
610
+ if any(field.original_name == parameter_name for field in fields):
611
+ msg = f"Parameter name '{parameter_name}' is used more than once."
612
+ raise Exception(msg) # noqa: TRY002
613
+
614
+ field_name, alias = self.model_resolver.get_valid_field_name_and_alias(
615
+ field_name=parameter_name,
616
+ excludes=exclude_field_names,
617
+ model_type=self.field_name_model_type,
618
+ class_name=name,
619
+ )
620
+ if parameter.schema_:
621
+ fields.append(
622
+ self.get_object_field(
623
+ field_name=field_name,
624
+ field=parameter.schema_,
625
+ field_type=self.parse_item(field_name, parameter.schema_, [*path, name, parameter_name]),
626
+ original_field_name=parameter_name,
627
+ required=parameter.required,
628
+ alias=alias,
629
+ )
630
+ )
631
+ else:
632
+ data_types: list[DataType] = []
633
+ object_schema: JsonSchemaObject | None = None
634
+ for (
635
+ media_type,
636
+ media_obj,
637
+ ) in parameter.content.items():
638
+ if not media_obj.schema_:
639
+ continue
640
+ object_schema = self.resolve_object(media_obj.schema_, JsonSchemaObject)
641
+ data_types.append(
642
+ self.parse_item(
643
+ field_name,
644
+ object_schema,
645
+ [*path, name, parameter_name, media_type],
646
+ )
315
647
  )
648
+
649
+ if not data_types:
650
+ continue
651
+ if len(data_types) == 1:
652
+ data_type = data_types[0]
316
653
  else:
317
- data_types[status_code][content_type] = self.get_ref_data_type(
318
- object_schema.ref
654
+ data_type = self.data_type(data_types=data_types)
655
+ # multiple data_type parse as non-constraints field
656
+ object_schema = None
657
+ fields.append(
658
+ self.data_model_field_type(
659
+ name=field_name,
660
+ default=object_schema.default if object_schema else None,
661
+ data_type=data_type,
662
+ required=parameter.required,
663
+ alias=alias,
664
+ constraints=object_schema.dict()
665
+ if object_schema and self.is_constraints_field(object_schema)
666
+ else None,
667
+ nullable=object_schema.nullable
668
+ if object_schema and self.strict_nullable and (object_schema.has_default or parameter.required)
669
+ else None,
670
+ strip_default_none=self.strip_default_none,
671
+ extras=self.get_field_extras(object_schema) if object_schema else {},
672
+ use_annotated=self.use_annotated,
673
+ use_serialize_as_any=self.use_serialize_as_any,
674
+ use_field_description=self.use_field_description,
675
+ use_inline_field_description=self.use_inline_field_description,
676
+ use_default_kwarg=self.use_default_kwarg,
677
+ original_name=parameter_name,
678
+ has_default=object_schema.has_default if object_schema else False,
679
+ type_has_null=object_schema.type_has_null if object_schema else None,
319
680
  )
681
+ )
320
682
 
321
- return data_types
683
+ if OpenAPIScope.Parameters in self.open_api_scopes and fields:
684
+ # Using _create_data_model from parent class JsonSchemaParser
685
+ # This method automatically adds frozen=True for DataClass types
686
+ self.results.append(
687
+ self._create_data_model(
688
+ fields=fields,
689
+ reference=reference,
690
+ custom_base_class=self.base_class,
691
+ custom_template_dir=self.custom_template_dir,
692
+ keyword_only=self.keyword_only,
693
+ treat_dot_as_module=self.treat_dot_as_module,
694
+ dataclass_arguments=self.dataclass_arguments,
695
+ )
696
+ )
697
+ return self.data_type(reference=reference)
322
698
 
323
- @classmethod
324
- def _get_model_name(cls, path_name: str, method: str, suffix: str) -> str:
325
- camel_path_name = snake_to_upper_camel(path_name.replace('/', '_'))
326
- return f'{camel_path_name}{method.capitalize()}{suffix}'
699
+ return None
327
700
 
328
701
  def parse_operation(
329
702
  self,
330
- raw_operation: Dict[str, Any],
331
- path: List[str],
703
+ raw_operation: dict[str, Any],
704
+ path: list[str],
332
705
  ) -> None:
706
+ """Parse an OpenAPI operation including parameters, request body, and responses."""
333
707
  operation = Operation.parse_obj(raw_operation)
334
- for parameters in operation.parameters:
335
- if isinstance(parameters, ReferenceObject):
336
- ref_parameter = self.get_ref_model(parameters.ref)
337
- parameters = ParameterObject.parse_obj(ref_parameter)
338
- self.parse_parameters(parameters=parameters, path=[*path, 'parameters'])
339
708
  path_name, method = path[-2:]
709
+ if self.use_operation_id_as_name:
710
+ if not operation.operationId:
711
+ msg = (
712
+ f"All operations must have an operationId when --use_operation_id_as_name is set."
713
+ f"The following path was missing an operationId: {path_name}"
714
+ )
715
+ raise Error(msg)
716
+ path_name = operation.operationId
717
+ method = ""
718
+ self.parse_all_parameters(
719
+ self._get_model_name(
720
+ path_name, method, suffix="Parameters" if self.include_path_parameters else "ParametersQuery"
721
+ ),
722
+ operation.parameters,
723
+ [*path, "parameters"],
724
+ )
340
725
  if operation.requestBody:
341
726
  if isinstance(operation.requestBody, ReferenceObject):
342
727
  ref_model = self.get_ref_model(operation.requestBody.ref)
@@ -344,78 +729,102 @@ class OpenAPIParser(JsonSchemaParser):
344
729
  else:
345
730
  request_body = operation.requestBody
346
731
  self.parse_request_body(
347
- name=self._get_model_name(path_name, method, suffix='Request'),
732
+ name=self._get_model_name(path_name, method, suffix="Request"),
348
733
  request_body=request_body,
349
- path=[*path, 'requestBody'],
734
+ path=[*path, "requestBody"],
350
735
  )
351
736
  self.parse_responses(
352
- name=self._get_model_name(path_name, method, suffix='Response'),
737
+ name=self._get_model_name(path_name, method, suffix="Response"),
353
738
  responses=operation.responses,
354
- path=[*path, 'responses'],
739
+ path=[*path, "responses"],
355
740
  )
741
+ if OpenAPIScope.Tags in self.open_api_scopes:
742
+ self.parse_tags(
743
+ name=self._get_model_name(path_name, method, suffix="Tags"),
744
+ tags=operation.tags,
745
+ path=[*path, "tags"],
746
+ )
356
747
 
357
748
  def parse_raw(self) -> None:
358
- for source in self.iter_source:
749
+ """Parse OpenAPI specification including schemas, paths, and operations."""
750
+ for source, path_parts in self._get_context_source_path_parts():
359
751
  if self.validation:
360
- from prance import BaseParser
361
-
362
- BaseParser(
363
- spec_string=source.text,
364
- backend='openapi-spec-validator',
365
- encoding=self.encoding,
752
+ warn(
753
+ "Deprecated: `--validation` option is deprecated. the option will be removed in a future "
754
+ "release. please use another tool to validate OpenAPI.\n",
755
+ stacklevel=2,
366
756
  )
367
- specification: Dict[str, Any] = load_yaml(source.text)
757
+
758
+ try:
759
+ from prance import BaseParser # noqa: PLC0415
760
+
761
+ BaseParser(
762
+ spec_string=source.text,
763
+ backend="openapi-spec-validator",
764
+ encoding=self.encoding,
765
+ )
766
+ except ImportError: # pragma: no cover
767
+ warn(
768
+ "Warning: Validation was skipped for OpenAPI. `prance` or `openapi-spec-validator` are not "
769
+ "installed.\n"
770
+ "To use --validation option after datamodel-code-generator 0.24.0, Please run `$pip install "
771
+ "'datamodel-code-generator[validation]'`.\n",
772
+ stacklevel=2,
773
+ )
774
+
775
+ specification: dict[str, Any] = load_yaml_dict(source.text)
368
776
  self.raw_obj = specification
369
- schemas: Dict[Any, Any] = specification.get('components', {}).get(
370
- 'schemas', {}
371
- )
372
- security: Optional[List[Dict[str, List[str]]]] = specification.get(
373
- 'security'
374
- )
375
- if isinstance(self.source, ParseResult):
376
- path_parts: List[str] = self.get_url_path_parts(self.source)
377
- else:
378
- path_parts = list(source.path.parts)
379
- with self.model_resolver.current_root_context(path_parts):
380
- if OpenAPIScope.Schemas in self.open_api_scopes:
381
- for (
777
+ self._collect_discriminator_schemas()
778
+ schemas: dict[str, Any] = specification.get("components", {}).get("schemas", {})
779
+ paths: dict[str, Any] = specification.get("paths", {})
780
+ security: list[dict[str, list[str]]] | None = specification.get("security")
781
+ # Warn if schemas is empty but paths exist and only Schemas scope is used
782
+ if not schemas and self.open_api_scopes == [OpenAPIScope.Schemas] and paths:
783
+ warn(
784
+ "No schemas found in components/schemas. If your schemas are defined in "
785
+ "external files referenced from paths, consider using --openapi-scopes paths",
786
+ stacklevel=2,
787
+ )
788
+ if OpenAPIScope.Schemas in self.open_api_scopes:
789
+ for obj_name, raw_obj in schemas.items():
790
+ self.parse_raw_obj(
382
791
  obj_name,
383
792
  raw_obj,
384
- ) in schemas.items(): # type: str, Dict[Any, Any]
385
- self.parse_raw_obj(
386
- obj_name,
387
- raw_obj,
388
- [*path_parts, '#/components', 'schemas', obj_name],
389
- )
390
- if OpenAPIScope.Paths in self.open_api_scopes:
391
- paths: Dict[str, Dict[str, Any]] = specification.get('paths', {})
392
- parameters: List[Dict[str, Any]] = [
393
- self._get_ref_body(p['$ref']) if '$ref' in p else p # type: ignore
394
- for p in paths.get('parameters', [])
395
- if isinstance(p, dict)
396
- ]
397
- paths_path = [*path_parts, '#/paths']
398
- for path_name, methods in paths.items():
399
-
400
- paths_parameters = parameters[:]
401
- if 'parameters' in methods:
402
- paths_parameters.extend(methods['parameters'])
403
- relative_path_name = path_name[1:]
404
- if relative_path_name:
405
- path = [*paths_path, relative_path_name]
406
- else: # pragma: no cover
407
- path = get_special_path('root', paths_path)
408
- for operation_name, raw_operation in methods.items():
409
- if operation_name not in OPERATION_NAMES:
410
- continue
411
- if paths_parameters:
412
- if 'parameters' in raw_operation: # pragma: no cover
413
- raw_operation['parameters'].extend(paths_parameters)
414
- else:
415
- raw_operation['parameters'] = paths_parameters
416
- if security is not None and 'security' not in raw_operation:
417
- raw_operation['security'] = security
418
- self.parse_operation(
419
- raw_operation,
420
- [*path, operation_name],
421
- )
793
+ [*path_parts, "#/components", "schemas", obj_name],
794
+ )
795
+ if OpenAPIScope.Paths in self.open_api_scopes:
796
+ # Resolve $ref in global parameter list
797
+ global_parameters = [
798
+ self._get_ref_body(p["$ref"]) if isinstance(p, dict) and "$ref" in p else p
799
+ for p in paths.get("parameters", [])
800
+ if isinstance(p, dict)
801
+ ]
802
+ self._process_path_items(paths, path_parts, "paths", global_parameters, security)
803
+
804
+ if OpenAPIScope.Webhooks in self.open_api_scopes:
805
+ webhooks: dict[str, dict[str, Any]] = specification.get("webhooks", {})
806
+ self._process_path_items(webhooks, path_parts, "webhooks", [], security, strip_leading_slash=False)
807
+
808
+ self._resolve_unparsed_json_pointer()
809
+
810
+ def _collect_discriminator_schemas(self) -> None:
811
+ """Collect schemas with discriminators but no oneOf/anyOf, and find their subtypes."""
812
+ schemas: dict[str, Any] = self.raw_obj.get("components", {}).get("schemas", {})
813
+
814
+ for schema_name, schema in schemas.items():
815
+ discriminator = schema.get("discriminator")
816
+ if not discriminator:
817
+ continue
818
+
819
+ if schema.get("oneOf") or schema.get("anyOf"):
820
+ continue
821
+
822
+ ref = f"#/components/schemas/{schema_name}"
823
+ self._discriminator_schemas[ref] = discriminator
824
+
825
+ for schema_name, schema in schemas.items():
826
+ for all_of_item in schema.get("allOf", []):
827
+ ref_in_allof = all_of_item.get("$ref")
828
+ if ref_in_allof and ref_in_allof in self._discriminator_schemas:
829
+ subtype_ref = f"#/components/schemas/{schema_name}"
830
+ self._discriminator_subtypes[ref_in_allof].append(subtype_ref)