diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index 1c84597f0..6683b2f13 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -43,9 +43,11 @@ GraphQLScope, InputFileType, InputModelRefStrategy, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, @@ -966,10 +968,12 @@ def __getattr__(name: str) -> Any: "InputModelRefStrategy", "InvalidClassNameError", "InvalidFileFormatError", + "JsonSchemaVersion", "LiteralType", "ModuleSplitMode", "NamingStrategy", "OpenAPIScope", + "OpenAPIVersion", "PythonVersion", "PythonVersionMin", "ReadOnlyWriteOnlyModelType", diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index cfc52f236..c35731a29 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -63,9 +63,11 @@ InputFileType, InputModelRefStrategy, InvalidClassNameError, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, @@ -491,6 +493,8 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) -> input_model: Optional[list[str]] = None # noqa: UP045 input_model_ref_strategy: Optional[InputModelRefStrategy] = None # noqa: UP045 input_file_type: InputFileType = InputFileType.Auto + jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto + openapi_version: OpenAPIVersion = OpenAPIVersion.Auto output_model_type: DataModelType = DataModelType.PydanticBaseModel output: Optional[Path] = None # noqa: UP045 check: bool = False @@ -938,6 +942,8 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917 result = generate( input_=input_, input_file_type=config.input_file_type, + jsonschema_version=config.jsonschema_version, + openapi_version=config.openapi_version, output=output, output_model_type=config.output_model_type, target_python_version=config.target_python_version, diff --git a/src/datamodel_code_generator/_types/generate_config_dict.py b/src/datamodel_code_generator/_types/generate_config_dict.py index ce0c52847..6bcb48f74 100644 --- a/src/datamodel_code_generator/_types/generate_config_dict.py +++ b/src/datamodel_code_generator/_types/generate_config_dict.py @@ -24,9 +24,11 @@ FieldTypeCollisionStrategy, GraphQLScope, InputFileType, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, StrictTypes, @@ -41,6 +43,8 @@ class GenerateConfigDict(TypedDict): input_filename: NotRequired[str | None] input_file_type: NotRequired[InputFileType] + jsonschema_version: NotRequired[JsonSchemaVersion] + openapi_version: NotRequired[OpenAPIVersion] output: NotRequired[Path | None] output_model_type: NotRequired[DataModelType] target_python_version: NotRequired[PythonVersion] diff --git a/src/datamodel_code_generator/_types/parser_config_dicts.py b/src/datamodel_code_generator/_types/parser_config_dicts.py index f63151bd1..fbcf0300b 100644 --- a/src/datamodel_code_generator/_types/parser_config_dicts.py +++ b/src/datamodel_code_generator/_types/parser_config_dicts.py @@ -19,8 +19,10 @@ CollapseRootModelsNameStrategy, DataclassArguments, FieldTypeCollisionStrategy, + JsonSchemaVersion, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, StrictTypes, @@ -45,6 +47,8 @@ class ParserConfigDict(TypedDict): data_model_root_type: NotRequired[type[DataModel]] data_type_manager_type: NotRequired[type[DataTypeManager]] data_model_field_type: NotRequired[type[DataModelFieldBase]] + jsonschema_version: NotRequired[JsonSchemaVersion] + openapi_version: NotRequired[OpenAPIVersion] base_class: NotRequired[str | None] base_class_map: NotRequired[dict[str, str] | None] additional_imports: NotRequired[list[str] | None] diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py index bdf680fb9..1b379b601 100644 --- a/src/datamodel_code_generator/arguments.py +++ b/src/datamodel_code_generator/arguments.py @@ -26,9 +26,11 @@ FieldTypeCollisionStrategy, InputFileType, InputModelRefStrategy, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, StrictTypes, @@ -146,6 +148,24 @@ def start_section(self, heading: str | None) -> None: ), choices=[i.value for i in InputFileType], ) +base_options.add_argument( + "--jsonschema-version", + help=( + "JSON Schema version (default: auto). " + "When 'auto', version is detected from $schema field. " + "Specify explicitly to override detection or when $schema is missing." + ), + choices=[v.value for v in JsonSchemaVersion], +) +base_options.add_argument( + "--openapi-version", + help=( + "OpenAPI version (default: auto). " + "When 'auto', version is detected from openapi/swagger field. " + "Specify explicitly to override detection." + ), + choices=[v.value for v in OpenAPIVersion], +) base_options.add_argument( "--output", help="Output file (default: stdout)", diff --git a/src/datamodel_code_generator/cli_options.py b/src/datamodel_code_generator/cli_options.py index 570cf73b0..8c7ba6416 100644 --- a/src/datamodel_code_generator/cli_options.py +++ b/src/datamodel_code_generator/cli_options.py @@ -52,6 +52,9 @@ class CLIOptionMeta: "--profile", "--no-color", "--generate-prompt", + # Schema version options - placeholders for future version-specific behavior + "--jsonschema-version", + "--openapi-version", }) # Backward compatibility alias diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index 18ca37980..a5d868cef 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -22,9 +22,11 @@ FieldTypeCollisionStrategy, GraphQLScope, InputFileType, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, @@ -78,6 +80,8 @@ class Config: input_filename: str | None = None input_file_type: InputFileType = InputFileType.Auto + jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto + openapi_version: OpenAPIVersion = OpenAPIVersion.Auto output: Path | None = None output_model_type: DataModelType = DataModelType.PydanticBaseModel target_python_version: PythonVersion = PythonVersionMin @@ -224,6 +228,8 @@ class Config: data_model_root_type: type[DataModel] = pydantic_model.CustomRootType data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField + jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto + openapi_version: OpenAPIVersion = OpenAPIVersion.Auto base_class: str | None = None base_class_map: dict[str, str] | None = None additional_imports: list[str] | None = None diff --git a/src/datamodel_code_generator/enums.py b/src/datamodel_code_generator/enums.py index 6d08489a1..b2426094a 100644 --- a/src/datamodel_code_generator/enums.py +++ b/src/datamodel_code_generator/enums.py @@ -240,6 +240,33 @@ class StrictTypes(Enum): bool = "bool" +class JsonSchemaVersion(Enum): + """JSON Schema draft versions. + + Used to specify which JSON Schema draft to use for parsing and validation. + Different drafts have different features and semantics. + """ + + Auto = "auto" + Draft04 = "draft-04" + Draft07 = "draft-07" + Draft201909 = "2019-09" + Draft202012 = "2020-12" + + +class OpenAPIVersion(Enum): + """OpenAPI specification versions. + + Used to specify which OpenAPI version to use for parsing. + Different versions have different schema semantics (e.g., nullable handling). + """ + + Auto = "auto" + V20 = "2.0" + V30 = "3.0" + V31 = "3.1" + + __all__ = [ "DEFAULT_SHARED_MODULE_NAME", "MAX_VERSION", @@ -256,9 +283,11 @@ class StrictTypes(Enum): "GraphQLScope", "InputFileType", "InputModelRefStrategy", + "JsonSchemaVersion", "ModuleSplitMode", "NamingStrategy", "OpenAPIScope", + "OpenAPIVersion", "ReadOnlyWriteOnlyModelType", "ReuseScope", "StrictTypes", diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index dc1bca6b5..1f9f5878e 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -806,6 +806,8 @@ def __init__( # noqa: PLR0912, PLR0915 self.use_subclass_enum: bool = config.use_subclass_enum self.use_specialized_enum: bool = config.use_specialized_enum self.strict_nullable: bool = config.strict_nullable + self.jsonschema_version = config.jsonschema_version + self.openapi_version = config.openapi_version self.use_generic_container_types: bool = config.use_generic_container_types self.use_union_operator: bool = config.use_union_operator self.enable_faux_immutability: bool = config.enable_faux_immutability diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 0142b77fc..293e92ab4 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -230,6 +230,7 @@ def get_data_type(self, obj: JsonSchemaObject) -> DataType: # https://swagger.io/docs/specification/data-models/data-types/#null # OpenAPI 3.1 does allow `null` in the `type` field and is equivalent to # a `nullable` flag on the property itself + # For backward compatibility, process nullable the same way for all versions if obj.nullable and self.strict_nullable and isinstance(obj.type, str): obj.type = [obj.type, "null"] diff --git a/tests/data/expected/main/input_model/config_class.py b/tests/data/expected/main/input_model/config_class.py index 09fc41050..b1e4adbdd 100644 --- a/tests/data/expected/main/input_model/config_class.py +++ b/tests/data/expected/main/input_model/config_class.py @@ -77,6 +77,11 @@ class DataclassArguments(TypedDict): ] +JsonSchemaVersion: TypeAlias = Literal[ + 'auto', 'draft-04', 'draft-07', '2019-09', '2020-12' +] + + LiteralType: TypeAlias = Literal['all', 'one', 'none'] @@ -93,6 +98,9 @@ class DataclassArguments(TypedDict): ] +OpenAPIVersion: TypeAlias = Literal['auto', '2.0', '3.0', '3.1'] + + PythonVersion: TypeAlias = Literal['3.10', '3.11', '3.12', '3.13', '3.14'] @@ -117,6 +125,8 @@ class DataclassArguments(TypedDict): class GenerateConfig(TypedDict): input_filename: NotRequired[str | None] input_file_type: NotRequired[InputFileType] + jsonschema_version: NotRequired[JsonSchemaVersion] + openapi_version: NotRequired[OpenAPIVersion] output: NotRequired[str | None] output_model_type: NotRequired[DataModelType] target_python_version: NotRequired[PythonVersion] diff --git a/tests/data/expected/main/openapi/openapi_version_nullable_v31.py b/tests/data/expected/main/openapi/openapi_version_nullable_v31.py new file mode 100644 index 000000000..c382ef97d --- /dev/null +++ b/tests/data/expected/main/openapi/openapi_version_nullable_v31.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: openapi_version_nullable.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel + + +class Pet(BaseModel): + name: str | None = None + age: int | None = None diff --git a/tests/data/openapi/openapi_version_nullable.yaml b/tests/data/openapi/openapi_version_nullable.yaml new file mode 100644 index 000000000..10a7401c2 --- /dev/null +++ b/tests/data/openapi/openapi_version_nullable.yaml @@ -0,0 +1,15 @@ +openapi: "3.1.0" +info: + title: Test API + version: "1.0.0" +paths: {} +components: + schemas: + Pet: + type: object + properties: + name: + type: string + nullable: true + age: + type: integer diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 0847d1cc2..0947a400f 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -4869,3 +4869,22 @@ def test_main_openapi_deprecated_field(output_file: Path) -> None: expected_file="deprecated_field.py", extra_args=["--output-model-type", "pydantic_v2.BaseModel"], ) + + +@SKIP_PYDANTIC_V1 +def test_main_openapi_version_nullable_v31(output_file: Path) -> None: + """Test that nullable is processed correctly in OpenAPI 3.1 mode with --openapi-version.""" + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "openapi_version_nullable.yaml", + output_path=output_file, + input_file_type="openapi", + assert_func=assert_file_content, + expected_file="openapi_version_nullable_v31.py", + extra_args=[ + "--output-model-type", + "pydantic_v2.BaseModel", + "--strict-nullable", + "--openapi-version", + "3.1", + ], + ) diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index f9fff5b1e..5e2dd2725 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -22,9 +22,11 @@ FieldTypeCollisionStrategy, GraphQLScope, InputFileType, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, @@ -60,6 +62,8 @@ def _baseline_generate( config: GenerateConfig | None = None, input_filename: str | None = None, input_file_type: InputFileType = InputFileType.Auto, + jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto, + openapi_version: OpenAPIVersion = OpenAPIVersion.Auto, output: Path | None = None, output_model_type: DataModelType = DataModelType.PydanticBaseModel, target_python_version: PythonVersion = PythonVersionMin, @@ -200,6 +204,8 @@ def __init__( data_model_root_type: type[DataModel] = pydantic_model.CustomRootType, data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField, + jsonschema_version: JsonSchemaVersion = JsonSchemaVersion.Auto, + openapi_version: OpenAPIVersion = OpenAPIVersion.Auto, base_class: str | None = None, base_class_map: dict[str, str] | None = None, additional_imports: list[str] | None = None,