From 27f10b410276192018f44d94958b0989c10f8632 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sun, 4 Jan 2026 05:57:16 +0000 Subject: [PATCH 1/3] Add --jsonschema-version and --openapi-version CLI options --- src/datamodel_code_generator/__init__.py | 75 ++++++++++++ src/datamodel_code_generator/__main__.py | 6 + .../_types/generate_config_dict.py | 4 + .../_types/parser_config_dicts.py | 4 + src/datamodel_code_generator/arguments.py | 20 ++++ src/datamodel_code_generator/cli_options.py | 3 + src/datamodel_code_generator/config.py | 6 + src/datamodel_code_generator/enums.py | 29 +++++ .../expected/main/input_model/config_class.py | 10 ++ tests/main/jsonschema/test_main_jsonschema.py | 110 ++++++++++++++++++ .../test_public_api_signature_baseline.py | 6 + 11 files changed, 273 insertions(+) diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index 1c84597f0..586173b10 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, @@ -288,6 +290,42 @@ def is_schema(data: dict) -> bool: return isinstance(data.get("properties"), dict) +def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: + """Detect JSON Schema version from $schema field. + + Returns Auto if version cannot be detected, allowing all features. + """ + schema = data.get("$schema", "") + if not isinstance(schema, str): + return JsonSchemaVersion.Auto + if "draft-04" in schema: + return JsonSchemaVersion.Draft04 + if "draft-07" in schema: + return JsonSchemaVersion.Draft07 + if "2019-09" in schema: + return JsonSchemaVersion.Draft201909 + if "2020-12" in schema: + return JsonSchemaVersion.Draft202012 + return JsonSchemaVersion.Auto + + +def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion: + """Detect OpenAPI version from openapi/swagger field. + + Returns Auto if version cannot be detected, allowing all features. + """ + if "swagger" in data: + return OpenAPIVersion.V20 + openapi = data.get("openapi", "") + if not isinstance(openapi, str): + return OpenAPIVersion.Auto + if openapi.startswith("3.1"): + return OpenAPIVersion.V31 + if openapi.startswith("3.0"): + return OpenAPIVersion.V30 + return OpenAPIVersion.Auto + + RAW_DATA_TYPES: list[InputFileType] = [ InputFileType.Json, InputFileType.Yaml, @@ -338,6 +376,35 @@ def __init__( super().__init__(message=message) +class SchemaVersionError(Error): + """Base exception for schema version-related errors.""" + + +class UnsupportedVersionError(SchemaVersionError): + """Raised when an unsupported schema version is encountered.""" + + def __init__(self, version: str, supported: list[str]) -> None: + """Initialize with version and list of supported versions.""" + self.version = version + self.supported = supported + message = f"Unsupported schema version: {version}. Supported versions: {', '.join(supported)}" + super().__init__(message=message) + + +class SchemaValidationError(SchemaVersionError): + """Raised when strict validation fails for a schema construct.""" + + def __init__(self, message: str, version: str, feature: str) -> None: + """Initialize with message, version, and feature name.""" + self.version = version + self.feature = feature + super().__init__(message=f"[{version}] {message}") + + +class VersionMismatchWarning(UserWarning): + """Warning for version-specific feature usage in wrong version.""" + + class SchemaParseError(Error): """Raised when an error occurs during schema parsing with path context.""" @@ -966,17 +1033,25 @@ def __getattr__(name: str) -> Any: "InputModelRefStrategy", "InvalidClassNameError", "InvalidFileFormatError", + "JsonSchemaVersion", "LiteralType", "ModuleSplitMode", "NamingStrategy", "OpenAPIScope", + "OpenAPIVersion", "PythonVersion", "PythonVersionMin", "ReadOnlyWriteOnlyModelType", "ReuseScope", "SchemaParseError", + "SchemaValidationError", + "SchemaVersionError", "TargetPydanticVersion", + "UnsupportedVersionError", + "VersionMismatchWarning", "clear_dynamic_models_cache", # noqa: F822 + "detect_jsonschema_version", + "detect_openapi_version", "generate", "generate_dynamic_models", # noqa: F822 ] 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/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/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 6dda2ee8b..371923e59 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -7903,3 +7903,113 @@ def test_validators_requires_pydantic_v2(output_file: Path, tmp_path: Path, caps capsys=capsys, expected_stderr_contains="--validators option requires Pydantic v2", ) + + +# ============================================================================= +# Schema Version Detection Tests +# ============================================================================= + + +class TestSchemaVersionDetection: + """Tests for schema version detection functions.""" + + def test_detect_jsonschema_version_draft04(self) -> None: + """Test detection of JSON Schema draft-04.""" + from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version + + data = {"$schema": "http://json-schema.org/draft-04/schema#"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft04 + + def test_detect_jsonschema_version_draft07(self) -> None: + """Test detection of JSON Schema draft-07.""" + from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version + + data = {"$schema": "http://json-schema.org/draft-07/schema#"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft07 + + def test_detect_jsonschema_version_2019_09(self) -> None: + """Test detection of JSON Schema 2019-09.""" + from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version + + data = {"$schema": "https://json-schema.org/draft/2019-09/schema"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft201909 + + def test_detect_jsonschema_version_2020_12(self) -> None: + """Test detection of JSON Schema 2020-12.""" + from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version + + data = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft202012 + + def test_detect_jsonschema_version_no_schema(self) -> None: + """Test detection with missing $schema defaults to Auto.""" + from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version + + data = {"type": "object"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Auto + + def test_detect_jsonschema_version_non_string_schema(self) -> None: + """Test detection with non-string $schema defaults to Auto.""" + from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version + + data = {"$schema": 123} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Auto + + def test_detect_openapi_version_swagger(self) -> None: + """Test detection of OpenAPI 2.0 (Swagger).""" + from datamodel_code_generator import OpenAPIVersion, detect_openapi_version + + data = {"swagger": "2.0"} + assert detect_openapi_version(data) == OpenAPIVersion.V20 + + def test_detect_openapi_version_30(self) -> None: + """Test detection of OpenAPI 3.0.""" + from datamodel_code_generator import OpenAPIVersion, detect_openapi_version + + data = {"openapi": "3.0.3"} + assert detect_openapi_version(data) == OpenAPIVersion.V30 + + def test_detect_openapi_version_31(self) -> None: + """Test detection of OpenAPI 3.1.""" + from datamodel_code_generator import OpenAPIVersion, detect_openapi_version + + data = {"openapi": "3.1.0"} + assert detect_openapi_version(data) == OpenAPIVersion.V31 + + def test_detect_openapi_version_no_version(self) -> None: + """Test detection with missing version field defaults to Auto.""" + from datamodel_code_generator import OpenAPIVersion, detect_openapi_version + + data = {"paths": {}} + assert detect_openapi_version(data) == OpenAPIVersion.Auto + + def test_detect_openapi_version_non_string(self) -> None: + """Test detection with non-string openapi defaults to Auto.""" + from datamodel_code_generator import OpenAPIVersion, detect_openapi_version + + data = {"openapi": 3.1} + assert detect_openapi_version(data) == OpenAPIVersion.Auto + + +class TestSchemaVersionExceptions: + """Tests for schema version exception classes.""" + + def test_unsupported_version_error(self) -> None: + """Test UnsupportedVersionError exception.""" + from datamodel_code_generator import UnsupportedVersionError + + error = UnsupportedVersionError("1.0", ["2.0", "3.0", "3.1"]) + assert error.version == "1.0" + assert error.supported == ["2.0", "3.0", "3.1"] + assert "1.0" in str(error) + assert "2.0" in str(error) + + def test_schema_validation_error(self) -> None: + """Test SchemaValidationError exception.""" + from datamodel_code_generator import SchemaValidationError + + error = SchemaValidationError("nullable not allowed", "3.1", "nullable") + assert error.version == "3.1" + assert error.feature == "nullable" + assert "[3.1]" in str(error) + assert "nullable not allowed" in str(error) 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, From 40ab6e4db211f2b58207be9aebf3fa075c1571e5 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sun, 4 Jan 2026 06:31:57 +0000 Subject: [PATCH 2/3] Add jsonschema_version and openapi_version to Parser --- src/datamodel_code_generator/parser/base.py | 2 ++ 1 file changed, 2 insertions(+) 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 From b5ca47452bb8d40d2324f48204bb27d4a2e6fbf5 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Sun, 4 Jan 2026 07:06:56 +0000 Subject: [PATCH 3/3] Remove unused version detection code and add e2e test --- src/datamodel_code_generator/__init__.py | 71 ----------- .../parser/openapi.py | 1 + .../openapi/openapi_version_nullable_v31.py | 12 ++ .../openapi/openapi_version_nullable.yaml | 15 +++ tests/main/jsonschema/test_main_jsonschema.py | 110 ------------------ tests/main/openapi/test_main_openapi.py | 19 +++ 6 files changed, 47 insertions(+), 181 deletions(-) create mode 100644 tests/data/expected/main/openapi/openapi_version_nullable_v31.py create mode 100644 tests/data/openapi/openapi_version_nullable.yaml diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index 586173b10..6683b2f13 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -290,42 +290,6 @@ def is_schema(data: dict) -> bool: return isinstance(data.get("properties"), dict) -def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: - """Detect JSON Schema version from $schema field. - - Returns Auto if version cannot be detected, allowing all features. - """ - schema = data.get("$schema", "") - if not isinstance(schema, str): - return JsonSchemaVersion.Auto - if "draft-04" in schema: - return JsonSchemaVersion.Draft04 - if "draft-07" in schema: - return JsonSchemaVersion.Draft07 - if "2019-09" in schema: - return JsonSchemaVersion.Draft201909 - if "2020-12" in schema: - return JsonSchemaVersion.Draft202012 - return JsonSchemaVersion.Auto - - -def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion: - """Detect OpenAPI version from openapi/swagger field. - - Returns Auto if version cannot be detected, allowing all features. - """ - if "swagger" in data: - return OpenAPIVersion.V20 - openapi = data.get("openapi", "") - if not isinstance(openapi, str): - return OpenAPIVersion.Auto - if openapi.startswith("3.1"): - return OpenAPIVersion.V31 - if openapi.startswith("3.0"): - return OpenAPIVersion.V30 - return OpenAPIVersion.Auto - - RAW_DATA_TYPES: list[InputFileType] = [ InputFileType.Json, InputFileType.Yaml, @@ -376,35 +340,6 @@ def __init__( super().__init__(message=message) -class SchemaVersionError(Error): - """Base exception for schema version-related errors.""" - - -class UnsupportedVersionError(SchemaVersionError): - """Raised when an unsupported schema version is encountered.""" - - def __init__(self, version: str, supported: list[str]) -> None: - """Initialize with version and list of supported versions.""" - self.version = version - self.supported = supported - message = f"Unsupported schema version: {version}. Supported versions: {', '.join(supported)}" - super().__init__(message=message) - - -class SchemaValidationError(SchemaVersionError): - """Raised when strict validation fails for a schema construct.""" - - def __init__(self, message: str, version: str, feature: str) -> None: - """Initialize with message, version, and feature name.""" - self.version = version - self.feature = feature - super().__init__(message=f"[{version}] {message}") - - -class VersionMismatchWarning(UserWarning): - """Warning for version-specific feature usage in wrong version.""" - - class SchemaParseError(Error): """Raised when an error occurs during schema parsing with path context.""" @@ -1044,14 +979,8 @@ def __getattr__(name: str) -> Any: "ReadOnlyWriteOnlyModelType", "ReuseScope", "SchemaParseError", - "SchemaValidationError", - "SchemaVersionError", "TargetPydanticVersion", - "UnsupportedVersionError", - "VersionMismatchWarning", "clear_dynamic_models_cache", # noqa: F822 - "detect_jsonschema_version", - "detect_openapi_version", "generate", "generate_dynamic_models", # noqa: F822 ] 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/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/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 371923e59..6dda2ee8b 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -7903,113 +7903,3 @@ def test_validators_requires_pydantic_v2(output_file: Path, tmp_path: Path, caps capsys=capsys, expected_stderr_contains="--validators option requires Pydantic v2", ) - - -# ============================================================================= -# Schema Version Detection Tests -# ============================================================================= - - -class TestSchemaVersionDetection: - """Tests for schema version detection functions.""" - - def test_detect_jsonschema_version_draft04(self) -> None: - """Test detection of JSON Schema draft-04.""" - from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version - - data = {"$schema": "http://json-schema.org/draft-04/schema#"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft04 - - def test_detect_jsonschema_version_draft07(self) -> None: - """Test detection of JSON Schema draft-07.""" - from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version - - data = {"$schema": "http://json-schema.org/draft-07/schema#"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft07 - - def test_detect_jsonschema_version_2019_09(self) -> None: - """Test detection of JSON Schema 2019-09.""" - from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version - - data = {"$schema": "https://json-schema.org/draft/2019-09/schema"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft201909 - - def test_detect_jsonschema_version_2020_12(self) -> None: - """Test detection of JSON Schema 2020-12.""" - from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version - - data = {"$schema": "https://json-schema.org/draft/2020-12/schema"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft202012 - - def test_detect_jsonschema_version_no_schema(self) -> None: - """Test detection with missing $schema defaults to Auto.""" - from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version - - data = {"type": "object"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Auto - - def test_detect_jsonschema_version_non_string_schema(self) -> None: - """Test detection with non-string $schema defaults to Auto.""" - from datamodel_code_generator import JsonSchemaVersion, detect_jsonschema_version - - data = {"$schema": 123} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Auto - - def test_detect_openapi_version_swagger(self) -> None: - """Test detection of OpenAPI 2.0 (Swagger).""" - from datamodel_code_generator import OpenAPIVersion, detect_openapi_version - - data = {"swagger": "2.0"} - assert detect_openapi_version(data) == OpenAPIVersion.V20 - - def test_detect_openapi_version_30(self) -> None: - """Test detection of OpenAPI 3.0.""" - from datamodel_code_generator import OpenAPIVersion, detect_openapi_version - - data = {"openapi": "3.0.3"} - assert detect_openapi_version(data) == OpenAPIVersion.V30 - - def test_detect_openapi_version_31(self) -> None: - """Test detection of OpenAPI 3.1.""" - from datamodel_code_generator import OpenAPIVersion, detect_openapi_version - - data = {"openapi": "3.1.0"} - assert detect_openapi_version(data) == OpenAPIVersion.V31 - - def test_detect_openapi_version_no_version(self) -> None: - """Test detection with missing version field defaults to Auto.""" - from datamodel_code_generator import OpenAPIVersion, detect_openapi_version - - data = {"paths": {}} - assert detect_openapi_version(data) == OpenAPIVersion.Auto - - def test_detect_openapi_version_non_string(self) -> None: - """Test detection with non-string openapi defaults to Auto.""" - from datamodel_code_generator import OpenAPIVersion, detect_openapi_version - - data = {"openapi": 3.1} - assert detect_openapi_version(data) == OpenAPIVersion.Auto - - -class TestSchemaVersionExceptions: - """Tests for schema version exception classes.""" - - def test_unsupported_version_error(self) -> None: - """Test UnsupportedVersionError exception.""" - from datamodel_code_generator import UnsupportedVersionError - - error = UnsupportedVersionError("1.0", ["2.0", "3.0", "3.1"]) - assert error.version == "1.0" - assert error.supported == ["2.0", "3.0", "3.1"] - assert "1.0" in str(error) - assert "2.0" in str(error) - - def test_schema_validation_error(self) -> None: - """Test SchemaValidationError exception.""" - from datamodel_code_generator import SchemaValidationError - - error = SchemaValidationError("nullable not allowed", "3.1", "nullable") - assert error.version == "3.1" - assert error.feature == "nullable" - assert "[3.1]" in str(error) - assert "nullable not allowed" in str(error) 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", + ], + )