Skip to content

Commit 27f10b4

Browse files
committed
Add --jsonschema-version and --openapi-version CLI options
1 parent 1b931d5 commit 27f10b4

File tree

11 files changed

+273
-0
lines changed

11 files changed

+273
-0
lines changed

src/datamodel_code_generator/__init__.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@
4343
GraphQLScope,
4444
InputFileType,
4545
InputModelRefStrategy,
46+
JsonSchemaVersion,
4647
ModuleSplitMode,
4748
NamingStrategy,
4849
OpenAPIScope,
50+
OpenAPIVersion,
4951
ReadOnlyWriteOnlyModelType,
5052
ReuseScope,
5153
TargetPydanticVersion,
@@ -288,6 +290,42 @@ def is_schema(data: dict) -> bool:
288290
return isinstance(data.get("properties"), dict)
289291

290292

293+
def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion:
294+
"""Detect JSON Schema version from $schema field.
295+
296+
Returns Auto if version cannot be detected, allowing all features.
297+
"""
298+
schema = data.get("$schema", "")
299+
if not isinstance(schema, str):
300+
return JsonSchemaVersion.Auto
301+
if "draft-04" in schema:
302+
return JsonSchemaVersion.Draft04
303+
if "draft-07" in schema:
304+
return JsonSchemaVersion.Draft07
305+
if "2019-09" in schema:
306+
return JsonSchemaVersion.Draft201909
307+
if "2020-12" in schema:
308+
return JsonSchemaVersion.Draft202012
309+
return JsonSchemaVersion.Auto
310+
311+
312+
def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion:
313+
"""Detect OpenAPI version from openapi/swagger field.
314+
315+
Returns Auto if version cannot be detected, allowing all features.
316+
"""
317+
if "swagger" in data:
318+
return OpenAPIVersion.V20
319+
openapi = data.get("openapi", "")
320+
if not isinstance(openapi, str):
321+
return OpenAPIVersion.Auto
322+
if openapi.startswith("3.1"):
323+
return OpenAPIVersion.V31
324+
if openapi.startswith("3.0"):
325+
return OpenAPIVersion.V30
326+
return OpenAPIVersion.Auto
327+
328+
291329
RAW_DATA_TYPES: list[InputFileType] = [
292330
InputFileType.Json,
293331
InputFileType.Yaml,
@@ -338,6 +376,35 @@ def __init__(
338376
super().__init__(message=message)
339377

340378

379+
class SchemaVersionError(Error):
380+
"""Base exception for schema version-related errors."""
381+
382+
383+
class UnsupportedVersionError(SchemaVersionError):
384+
"""Raised when an unsupported schema version is encountered."""
385+
386+
def __init__(self, version: str, supported: list[str]) -> None:
387+
"""Initialize with version and list of supported versions."""
388+
self.version = version
389+
self.supported = supported
390+
message = f"Unsupported schema version: {version}. Supported versions: {', '.join(supported)}"
391+
super().__init__(message=message)
392+
393+
394+
class SchemaValidationError(SchemaVersionError):
395+
"""Raised when strict validation fails for a schema construct."""
396+
397+
def __init__(self, message: str, version: str, feature: str) -> None:
398+
"""Initialize with message, version, and feature name."""
399+
self.version = version
400+
self.feature = feature
401+
super().__init__(message=f"[{version}] {message}")
402+
403+
404+
class VersionMismatchWarning(UserWarning):
405+
"""Warning for version-specific feature usage in wrong version."""
406+
407+
341408
class SchemaParseError(Error):
342409
"""Raised when an error occurs during schema parsing with path context."""
343410

@@ -966,17 +1033,25 @@ def __getattr__(name: str) -> Any:
9661033
"InputModelRefStrategy",
9671034
"InvalidClassNameError",
9681035
"InvalidFileFormatError",
1036+
"JsonSchemaVersion",
9691037
"LiteralType",
9701038
"ModuleSplitMode",
9711039
"NamingStrategy",
9721040
"OpenAPIScope",
1041+
"OpenAPIVersion",
9731042
"PythonVersion",
9741043
"PythonVersionMin",
9751044
"ReadOnlyWriteOnlyModelType",
9761045
"ReuseScope",
9771046
"SchemaParseError",
1047+
"SchemaValidationError",
1048+
"SchemaVersionError",
9781049
"TargetPydanticVersion",
1050+
"UnsupportedVersionError",
1051+
"VersionMismatchWarning",
9791052
"clear_dynamic_models_cache", # noqa: F822
1053+
"detect_jsonschema_version",
1054+
"detect_openapi_version",
9801055
"generate",
9811056
"generate_dynamic_models", # noqa: F822
9821057
]

src/datamodel_code_generator/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@
6363
InputFileType,
6464
InputModelRefStrategy,
6565
InvalidClassNameError,
66+
JsonSchemaVersion,
6667
ModuleSplitMode,
6768
NamingStrategy,
6869
OpenAPIScope,
70+
OpenAPIVersion,
6971
ReadOnlyWriteOnlyModelType,
7072
ReuseScope,
7173
TargetPydanticVersion,
@@ -491,6 +493,8 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) ->
491493
input_model: Optional[list[str]] = None # noqa: UP045
492494
input_model_ref_strategy: Optional[InputModelRefStrategy] = None # noqa: UP045
493495
input_file_type: InputFileType = InputFileType.Auto
496+
jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto
497+
openapi_version: OpenAPIVersion = OpenAPIVersion.Auto
494498
output_model_type: DataModelType = DataModelType.PydanticBaseModel
495499
output: Optional[Path] = None # noqa: UP045
496500
check: bool = False
@@ -938,6 +942,8 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
938942
result = generate(
939943
input_=input_,
940944
input_file_type=config.input_file_type,
945+
jsonschema_version=config.jsonschema_version,
946+
openapi_version=config.openapi_version,
941947
output=output,
942948
output_model_type=config.output_model_type,
943949
target_python_version=config.target_python_version,

src/datamodel_code_generator/_types/generate_config_dict.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
FieldTypeCollisionStrategy,
2525
GraphQLScope,
2626
InputFileType,
27+
JsonSchemaVersion,
2728
ModuleSplitMode,
2829
NamingStrategy,
2930
OpenAPIScope,
31+
OpenAPIVersion,
3032
ReadOnlyWriteOnlyModelType,
3133
ReuseScope,
3234
StrictTypes,
@@ -41,6 +43,8 @@
4143
class GenerateConfigDict(TypedDict):
4244
input_filename: NotRequired[str | None]
4345
input_file_type: NotRequired[InputFileType]
46+
jsonschema_version: NotRequired[JsonSchemaVersion]
47+
openapi_version: NotRequired[OpenAPIVersion]
4448
output: NotRequired[Path | None]
4549
output_model_type: NotRequired[DataModelType]
4650
target_python_version: NotRequired[PythonVersion]

src/datamodel_code_generator/_types/parser_config_dicts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
CollapseRootModelsNameStrategy,
2020
DataclassArguments,
2121
FieldTypeCollisionStrategy,
22+
JsonSchemaVersion,
2223
NamingStrategy,
2324
OpenAPIScope,
25+
OpenAPIVersion,
2426
ReadOnlyWriteOnlyModelType,
2527
ReuseScope,
2628
StrictTypes,
@@ -45,6 +47,8 @@ class ParserConfigDict(TypedDict):
4547
data_model_root_type: NotRequired[type[DataModel]]
4648
data_type_manager_type: NotRequired[type[DataTypeManager]]
4749
data_model_field_type: NotRequired[type[DataModelFieldBase]]
50+
jsonschema_version: NotRequired[JsonSchemaVersion]
51+
openapi_version: NotRequired[OpenAPIVersion]
4852
base_class: NotRequired[str | None]
4953
base_class_map: NotRequired[dict[str, str] | None]
5054
additional_imports: NotRequired[list[str] | None]

src/datamodel_code_generator/arguments.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
FieldTypeCollisionStrategy,
2727
InputFileType,
2828
InputModelRefStrategy,
29+
JsonSchemaVersion,
2930
ModuleSplitMode,
3031
NamingStrategy,
3132
OpenAPIScope,
33+
OpenAPIVersion,
3234
ReadOnlyWriteOnlyModelType,
3335
ReuseScope,
3436
StrictTypes,
@@ -146,6 +148,24 @@ def start_section(self, heading: str | None) -> None:
146148
),
147149
choices=[i.value for i in InputFileType],
148150
)
151+
base_options.add_argument(
152+
"--jsonschema-version",
153+
help=(
154+
"JSON Schema version (default: auto). "
155+
"When 'auto', version is detected from $schema field. "
156+
"Specify explicitly to override detection or when $schema is missing."
157+
),
158+
choices=[v.value for v in JsonSchemaVersion],
159+
)
160+
base_options.add_argument(
161+
"--openapi-version",
162+
help=(
163+
"OpenAPI version (default: auto). "
164+
"When 'auto', version is detected from openapi/swagger field. "
165+
"Specify explicitly to override detection."
166+
),
167+
choices=[v.value for v in OpenAPIVersion],
168+
)
149169
base_options.add_argument(
150170
"--output",
151171
help="Output file (default: stdout)",

src/datamodel_code_generator/cli_options.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class CLIOptionMeta:
5252
"--profile",
5353
"--no-color",
5454
"--generate-prompt",
55+
# Schema version options - placeholders for future version-specific behavior
56+
"--jsonschema-version",
57+
"--openapi-version",
5558
})
5659

5760
# Backward compatibility alias

src/datamodel_code_generator/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
FieldTypeCollisionStrategy,
2323
GraphQLScope,
2424
InputFileType,
25+
JsonSchemaVersion,
2526
ModuleSplitMode,
2627
NamingStrategy,
2728
OpenAPIScope,
29+
OpenAPIVersion,
2830
ReadOnlyWriteOnlyModelType,
2931
ReuseScope,
3032
TargetPydanticVersion,
@@ -78,6 +80,8 @@ class Config:
7880

7981
input_filename: str | None = None
8082
input_file_type: InputFileType = InputFileType.Auto
83+
jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto
84+
openapi_version: OpenAPIVersion = OpenAPIVersion.Auto
8185
output: Path | None = None
8286
output_model_type: DataModelType = DataModelType.PydanticBaseModel
8387
target_python_version: PythonVersion = PythonVersionMin
@@ -224,6 +228,8 @@ class Config:
224228
data_model_root_type: type[DataModel] = pydantic_model.CustomRootType
225229
data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager
226230
data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField
231+
jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto
232+
openapi_version: OpenAPIVersion = OpenAPIVersion.Auto
227233
base_class: str | None = None
228234
base_class_map: dict[str, str] | None = None
229235
additional_imports: list[str] | None = None

src/datamodel_code_generator/enums.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,33 @@ class StrictTypes(Enum):
240240
bool = "bool"
241241

242242

243+
class JsonSchemaVersion(Enum):
244+
"""JSON Schema draft versions.
245+
246+
Used to specify which JSON Schema draft to use for parsing and validation.
247+
Different drafts have different features and semantics.
248+
"""
249+
250+
Auto = "auto"
251+
Draft04 = "draft-04"
252+
Draft07 = "draft-07"
253+
Draft201909 = "2019-09"
254+
Draft202012 = "2020-12"
255+
256+
257+
class OpenAPIVersion(Enum):
258+
"""OpenAPI specification versions.
259+
260+
Used to specify which OpenAPI version to use for parsing.
261+
Different versions have different schema semantics (e.g., nullable handling).
262+
"""
263+
264+
Auto = "auto"
265+
V20 = "2.0"
266+
V30 = "3.0"
267+
V31 = "3.1"
268+
269+
243270
__all__ = [
244271
"DEFAULT_SHARED_MODULE_NAME",
245272
"MAX_VERSION",
@@ -256,9 +283,11 @@ class StrictTypes(Enum):
256283
"GraphQLScope",
257284
"InputFileType",
258285
"InputModelRefStrategy",
286+
"JsonSchemaVersion",
259287
"ModuleSplitMode",
260288
"NamingStrategy",
261289
"OpenAPIScope",
290+
"OpenAPIVersion",
262291
"ReadOnlyWriteOnlyModelType",
263292
"ReuseScope",
264293
"StrictTypes",

tests/data/expected/main/input_model/config_class.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ class DataclassArguments(TypedDict):
7777
]
7878

7979

80+
JsonSchemaVersion: TypeAlias = Literal[
81+
'auto', 'draft-04', 'draft-07', '2019-09', '2020-12'
82+
]
83+
84+
8085
LiteralType: TypeAlias = Literal['all', 'one', 'none']
8186

8287

@@ -93,6 +98,9 @@ class DataclassArguments(TypedDict):
9398
]
9499

95100

101+
OpenAPIVersion: TypeAlias = Literal['auto', '2.0', '3.0', '3.1']
102+
103+
96104
PythonVersion: TypeAlias = Literal['3.10', '3.11', '3.12', '3.13', '3.14']
97105

98106

@@ -117,6 +125,8 @@ class DataclassArguments(TypedDict):
117125
class GenerateConfig(TypedDict):
118126
input_filename: NotRequired[str | None]
119127
input_file_type: NotRequired[InputFileType]
128+
jsonschema_version: NotRequired[JsonSchemaVersion]
129+
openapi_version: NotRequired[OpenAPIVersion]
120130
output: NotRequired[str | None]
121131
output_model_type: NotRequired[DataModelType]
122132
target_python_version: NotRequired[PythonVersion]

0 commit comments

Comments
 (0)