From b1d78c6beafbc4a114c681cf86ba15e8351c313c Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 5 Jan 2026 16:01:35 +0000 Subject: [PATCH 1/5] Add schema version detection and feature flags --- src/datamodel_code_generator/__init__.py | 10 + src/datamodel_code_generator/enums.py | 39 +++ .../parser/schema_version.py | 194 +++++++++++++++ tests/parser/test_schema_version.py | 234 ++++++++++++++++++ 4 files changed, 477 insertions(+) create mode 100644 src/datamodel_code_generator/parser/schema_version.py create mode 100644 tests/parser/test_schema_version.py diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index 1c84597f0..2f0d39559 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -43,12 +43,15 @@ GraphQLScope, InputFileType, InputModelRefStrategy, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, + VersionMode, ) from datamodel_code_generator.format import ( DEFAULT_FORMATTERS, @@ -930,6 +933,8 @@ def infer_input_type(text: str) -> InputFileType: _LAZY_IMPORTS = { "clear_dynamic_models_cache": "datamodel_code_generator.dynamic", + "detect_jsonschema_version": "datamodel_code_generator.parser.schema_version", + "detect_openapi_version": "datamodel_code_generator.parser.schema_version", "generate_dynamic_models": "datamodel_code_generator.dynamic", } @@ -966,17 +971,22 @@ def __getattr__(name: str) -> Any: "InputModelRefStrategy", "InvalidClassNameError", "InvalidFileFormatError", + "JsonSchemaVersion", "LiteralType", "ModuleSplitMode", "NamingStrategy", "OpenAPIScope", + "OpenAPIVersion", "PythonVersion", "PythonVersionMin", "ReadOnlyWriteOnlyModelType", "ReuseScope", "SchemaParseError", "TargetPydanticVersion", + "VersionMode", "clear_dynamic_models_cache", # noqa: F822 + "detect_jsonschema_version", # noqa: F822 + "detect_openapi_version", # noqa: F822 "generate", "generate_dynamic_models", # noqa: F822 ] diff --git a/src/datamodel_code_generator/enums.py b/src/datamodel_code_generator/enums.py index 6d08489a1..456d139fe 100644 --- a/src/datamodel_code_generator/enums.py +++ b/src/datamodel_code_generator/enums.py @@ -240,6 +240,42 @@ class StrictTypes(Enum): bool = "bool" +class JsonSchemaVersion(Enum): + """JSON Schema draft versions. + + Auto: Auto-detect from $schema field or heuristics (default). + """ + + Draft4 = "draft-04" + Draft6 = "draft-06" + Draft7 = "draft-07" + Draft201909 = "2019-09" + Draft202012 = "2020-12" + Auto = "auto" + + +class OpenAPIVersion(Enum): + """OpenAPI specification versions. + + Auto: Auto-detect from openapi field (default). + """ + + V30 = "3.0" + V31 = "3.1" + Auto = "auto" + + +class VersionMode(Enum): + """Schema version validation mode. + + Lenient: Accept all features regardless of declared version (default). + Strict: Warn on features outside declared/detected version. + """ + + Lenient = "lenient" + Strict = "strict" + + __all__ = [ "DEFAULT_SHARED_MODULE_NAME", "MAX_VERSION", @@ -256,12 +292,15 @@ class StrictTypes(Enum): "GraphQLScope", "InputFileType", "InputModelRefStrategy", + "JsonSchemaVersion", "ModuleSplitMode", "NamingStrategy", "OpenAPIScope", + "OpenAPIVersion", "ReadOnlyWriteOnlyModelType", "ReuseScope", "StrictTypes", "TargetPydanticVersion", "UnionMode", + "VersionMode", ] diff --git a/src/datamodel_code_generator/parser/schema_version.py b/src/datamodel_code_generator/parser/schema_version.py new file mode 100644 index 000000000..b9fb44939 --- /dev/null +++ b/src/datamodel_code_generator/parser/schema_version.py @@ -0,0 +1,194 @@ +"""Schema version features and detection utilities. + +Provides SchemaFeatures classes for version-dependent feature flags +and utility functions for detecting schema versions. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, TypeVar + +from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion + + +@dataclass(frozen=True) +class JsonSchemaFeatures: + """Feature flags for JSON Schema versions. + + This is the base class for schema features. OpenAPISchemaFeatures + extends this to add OpenAPI-specific features. + + Attributes: + null_in_type_array: Draft 2020-12 allows null in type arrays. + defs_not_definitions: Draft 2019-09+ uses $defs instead of definitions. + prefix_items: Draft 2020-12 uses prefixItems instead of items array. + boolean_schemas: Draft 6+ allows boolean values as schemas. + id_field: The field name for schema ID ("id" for Draft 4, "$id" for Draft 6+). + definitions_key: The key for definitions ("definitions" or "$defs"). + """ + + null_in_type_array: bool + defs_not_definitions: bool + prefix_items: bool + boolean_schemas: bool + id_field: str + definitions_key: str + + @classmethod + def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: + """Create JsonSchemaFeatures from a JSON Schema version.""" + if version == JsonSchemaVersion.Draft4: + return cls( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=False, + id_field="id", + definitions_key="definitions", + ) + if version in {JsonSchemaVersion.Draft6, JsonSchemaVersion.Draft7}: + return cls( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=True, + id_field="$id", + definitions_key="definitions", + ) + if version == JsonSchemaVersion.Draft201909: + return cls( + null_in_type_array=False, + defs_not_definitions=True, + prefix_items=False, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) + # Draft 2020-12 or Auto (default to latest features in lenient mode) + return cls( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) + + +@dataclass(frozen=True) +class OpenAPISchemaFeatures(JsonSchemaFeatures): + """Feature flags for OpenAPI versions. + + Extends JsonSchemaFeatures with OpenAPI-specific features. + + Attributes: + nullable_keyword: OpenAPI 3.0 uses nullable: true (deprecated in 3.1). + discriminator_support: All OpenAPI versions support discriminator. + """ + + nullable_keyword: bool + discriminator_support: bool + + @classmethod + def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures: + """Create OpenAPISchemaFeatures from an OpenAPI version.""" + if version == OpenAPIVersion.V30: + # OpenAPI 3.0 does not support boolean schemas (only 3.1+ does) + return cls( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=False, + id_field="$id", + definitions_key="definitions", + nullable_keyword=True, + discriminator_support=True, + ) + # OpenAPI 3.1 or Auto (default to latest features in lenient mode) + return cls( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + nullable_keyword=False, + discriminator_support=True, + ) + + +# Type variable for SchemaFeatures subclasses +SchemaFeaturesT = TypeVar("SchemaFeaturesT", bound=JsonSchemaFeatures) + + +def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: # noqa: PLR0911 + """Detect JSON Schema version from $schema field or heuristics. + + Detection priority: + 1. $schema field explicit declaration + 2. Heuristics ($defs vs definitions, etc.) + 3. Fallback: Draft7 (most widely used) + + Note: In Lenient mode, detection result is only used for optimization hints. + In Strict mode, detection result is used to warn on version violations. + + Args: + data: The schema dictionary to analyze. + + Returns: + The detected JSON Schema version. + """ + schema_url = data.get("$schema", "") + if isinstance(schema_url, str): + if "draft-04" in schema_url: + return JsonSchemaVersion.Draft4 + if "draft-06" in schema_url: + return JsonSchemaVersion.Draft6 + if "draft-07" in schema_url: + return JsonSchemaVersion.Draft7 + if "2019-09" in schema_url: + return JsonSchemaVersion.Draft201909 + if "2020-12" in schema_url: + return JsonSchemaVersion.Draft202012 + + # Heuristic detection (when $schema is missing) + if "$defs" in data: + # $defs was introduced in Draft 2019-09 + # prefixItems is specific to Draft 2020-12 + if "prefixItems" in data: + return JsonSchemaVersion.Draft202012 + return JsonSchemaVersion.Draft201909 + if "definitions" in data: + return JsonSchemaVersion.Draft7 + + # Fallback: Draft7 is the most widely used + return JsonSchemaVersion.Draft7 + + +def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion: + """Detect OpenAPI version from openapi field. + + Args: + data: The schema dictionary to analyze. + + Returns: + The detected OpenAPI version. + """ + version = data.get("openapi", "") + if isinstance(version, str): + if version.startswith("3.1"): + return OpenAPIVersion.V31 + if version.startswith("3.0"): + return OpenAPIVersion.V30 + # Fallback: 3.1 (latest, best JSON Schema compatibility) + return OpenAPIVersion.V31 + + +__all__ = [ + "JsonSchemaFeatures", + "OpenAPISchemaFeatures", + "SchemaFeaturesT", + "detect_jsonschema_version", + "detect_openapi_version", +] diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py new file mode 100644 index 000000000..4ac5a7a10 --- /dev/null +++ b/tests/parser/test_schema_version.py @@ -0,0 +1,234 @@ +"""Tests for schema version detection and features.""" + +from __future__ import annotations + +import pytest + +import datamodel_code_generator +from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion +from datamodel_code_generator.parser.schema_version import ( + JsonSchemaFeatures, + OpenAPISchemaFeatures, + detect_jsonschema_version, + detect_openapi_version, +) + + +class TestDetectJsonSchemaVersion: + """Tests for detect_jsonschema_version function.""" + + def test_detect_draft4_from_schema(self) -> None: + """Test detection of Draft 4 from $schema field.""" + data = {"$schema": "http://json-schema.org/draft-04/schema#"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft4 + + def test_detect_draft6_from_schema(self) -> None: + """Test detection of Draft 6 from $schema field.""" + data = {"$schema": "http://json-schema.org/draft-06/schema#"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft6 + + def test_detect_draft7_from_schema(self) -> None: + """Test detection of Draft 7 from $schema field.""" + data = {"$schema": "http://json-schema.org/draft-07/schema#"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 + + def test_detect_2019_09_from_schema(self) -> None: + """Test detection of Draft 2019-09 from $schema field.""" + data = {"$schema": "https://json-schema.org/draft/2019-09/schema"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft201909 + + def test_detect_2020_12_from_schema(self) -> None: + """Test detection of Draft 2020-12 from $schema field.""" + data = {"$schema": "https://json-schema.org/draft/2020-12/schema"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft202012 + + def test_detect_from_defs_heuristic_with_prefix_items(self) -> None: + """Test detection using $defs with prefixItems heuristic.""" + data = {"$defs": {"Foo": {"type": "string"}}, "prefixItems": [{"type": "string"}]} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft202012 + + def test_detect_from_defs_heuristic_without_prefix_items(self) -> None: + """Test detection using $defs without prefixItems heuristic.""" + data = {"$defs": {"Foo": {"type": "string"}}} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft201909 + + def test_detect_from_definitions_heuristic(self) -> None: + """Test detection using definitions heuristic.""" + data = {"definitions": {"Foo": {"type": "string"}}} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 + + def test_fallback_to_draft7(self) -> None: + """Test fallback to Draft 7 when no indicators present.""" + data = {"type": "object"} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 + + def test_non_string_schema(self) -> None: + """Test handling of non-string $schema value.""" + data = {"$schema": 123} + assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 + + +class TestDetectOpenAPIVersion: + """Tests for detect_openapi_version function.""" + + def test_detect_openapi_30(self) -> None: + """Test detection of OpenAPI 3.0.""" + data = {"openapi": "3.0.0"} + assert detect_openapi_version(data) == OpenAPIVersion.V30 + + def test_detect_openapi_30_patch(self) -> None: + """Test detection of OpenAPI 3.0.x.""" + data = {"openapi": "3.0.3"} + assert detect_openapi_version(data) == OpenAPIVersion.V30 + + def test_detect_openapi_31(self) -> None: + """Test detection of OpenAPI 3.1.""" + data = {"openapi": "3.1.0"} + assert detect_openapi_version(data) == OpenAPIVersion.V31 + + def test_fallback_to_31(self) -> None: + """Test fallback to OpenAPI 3.1 when no version present.""" + data = {"info": {"title": "Test"}} + assert detect_openapi_version(data) == OpenAPIVersion.V31 + + def test_non_string_version(self) -> None: + """Test handling of non-string openapi value.""" + data = {"openapi": 3.0} + assert detect_openapi_version(data) == OpenAPIVersion.V31 + + +class TestJsonSchemaFeatures: + """Tests for JsonSchemaFeatures class.""" + + def test_from_version_draft4(self) -> None: + """Test Draft 4 features.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft4) + assert features.null_in_type_array is False + assert features.defs_not_definitions is False + assert features.prefix_items is False + assert features.boolean_schemas is False + assert features.id_field == "id" + assert features.definitions_key == "definitions" + + def test_from_version_draft6(self) -> None: + """Test Draft 6 features.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft6) + assert features.null_in_type_array is False + assert features.defs_not_definitions is False + assert features.prefix_items is False + assert features.boolean_schemas is True + assert features.id_field == "$id" + assert features.definitions_key == "definitions" + + def test_from_version_draft7(self) -> None: + """Test Draft 7 features.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft7) + assert features.null_in_type_array is False + assert features.defs_not_definitions is False + assert features.prefix_items is False + assert features.boolean_schemas is True + assert features.id_field == "$id" + assert features.definitions_key == "definitions" + + def test_from_version_2019_09(self) -> None: + """Test Draft 2019-09 features.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft201909) + assert features.null_in_type_array is False + assert features.defs_not_definitions is True + assert features.prefix_items is False + assert features.boolean_schemas is True + assert features.id_field == "$id" + assert features.definitions_key == "$defs" + + def test_from_version_2020_12(self) -> None: + """Test Draft 2020-12 features.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft202012) + assert features.null_in_type_array is True + assert features.defs_not_definitions is True + assert features.prefix_items is True + assert features.boolean_schemas is True + assert features.id_field == "$id" + assert features.definitions_key == "$defs" + + def test_from_version_auto(self) -> None: + """Test Auto version defaults to latest features.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Auto) + assert features.null_in_type_array is True + assert features.defs_not_definitions is True + assert features.prefix_items is True + assert features.boolean_schemas is True + + def test_frozen(self) -> None: + """Test that features are immutable.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft7) + with pytest.raises(AttributeError): + features.null_in_type_array = True # type: ignore[misc] + + +class TestOpenAPISchemaFeatures: + """Tests for OpenAPISchemaFeatures class.""" + + def test_from_openapi_version_v30(self) -> None: + """Test OpenAPI 3.0 features.""" + features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V30) + assert features.nullable_keyword is True + assert features.discriminator_support is True + assert features.boolean_schemas is False # OpenAPI 3.0 does not support boolean schemas + assert features.definitions_key == "definitions" + + def test_from_openapi_version_v31(self) -> None: + """Test OpenAPI 3.1 features.""" + features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V31) + assert features.nullable_keyword is False + assert features.discriminator_support is True + assert features.null_in_type_array is True + assert features.definitions_key == "$defs" + + def test_from_openapi_version_auto(self) -> None: + """Test Auto version defaults to latest features.""" + features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.Auto) + assert features.nullable_keyword is False + assert features.discriminator_support is True + assert features.null_in_type_array is True + + def test_inherits_jsonschema_features(self) -> None: + """Test that OpenAPISchemaFeatures inherits from JsonSchemaFeatures.""" + features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V31) + assert isinstance(features, JsonSchemaFeatures) + assert features.prefix_items is True + + def test_frozen(self) -> None: + """Test that features are immutable.""" + features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V30) + with pytest.raises(AttributeError): + features.nullable_keyword = False # type: ignore[misc] + + +class TestLazyImports: + """Tests for lazy imports from datamodel_code_generator module.""" + + def test_detect_jsonschema_version_lazy_import(self) -> None: + """Test that detect_jsonschema_version can be imported from main module.""" + detect_func = datamodel_code_generator.detect_jsonschema_version + result = detect_func({"$schema": "http://json-schema.org/draft-07/schema#"}) + assert result == JsonSchemaVersion.Draft7 + + def test_detect_openapi_version_lazy_import(self) -> None: + """Test that detect_openapi_version can be imported from main module.""" + detect_func = datamodel_code_generator.detect_openapi_version + result = detect_func({"openapi": "3.1.0"}) + assert result == OpenAPIVersion.V31 + + def test_jsonschema_version_enum_export(self) -> None: + """Test that JsonSchemaVersion is exported from main module.""" + assert datamodel_code_generator.JsonSchemaVersion is JsonSchemaVersion + + def test_openapi_version_enum_export(self) -> None: + """Test that OpenAPIVersion is exported from main module.""" + assert datamodel_code_generator.OpenAPIVersion is OpenAPIVersion + + def test_version_mode_enum_export(self) -> None: + """Test that VersionMode is exported from main module.""" + from datamodel_code_generator.enums import VersionMode + + assert datamodel_code_generator.VersionMode is VersionMode From 98ed8954c53c90d6e4d3c0a5a248c43d473bf2ca Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 5 Jan 2026 16:10:48 +0000 Subject: [PATCH 2/5] Refactor to use pattern matching and function-style tests --- .../parser/schema_version.py | 165 +++--- tests/parser/test_schema_version.py | 486 ++++++++++-------- 2 files changed, 345 insertions(+), 306 deletions(-) diff --git a/src/datamodel_code_generator/parser/schema_version.py b/src/datamodel_code_generator/parser/schema_version.py index b9fb44939..13edd8a2f 100644 --- a/src/datamodel_code_generator/parser/schema_version.py +++ b/src/datamodel_code_generator/parser/schema_version.py @@ -38,42 +38,43 @@ class JsonSchemaFeatures: @classmethod def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: """Create JsonSchemaFeatures from a JSON Schema version.""" - if version == JsonSchemaVersion.Draft4: - return cls( - null_in_type_array=False, - defs_not_definitions=False, - prefix_items=False, - boolean_schemas=False, - id_field="id", - definitions_key="definitions", - ) - if version in {JsonSchemaVersion.Draft6, JsonSchemaVersion.Draft7}: - return cls( - null_in_type_array=False, - defs_not_definitions=False, - prefix_items=False, - boolean_schemas=True, - id_field="$id", - definitions_key="definitions", - ) - if version == JsonSchemaVersion.Draft201909: - return cls( - null_in_type_array=False, - defs_not_definitions=True, - prefix_items=False, - boolean_schemas=True, - id_field="$id", - definitions_key="$defs", - ) - # Draft 2020-12 or Auto (default to latest features in lenient mode) - return cls( - null_in_type_array=True, - defs_not_definitions=True, - prefix_items=True, - boolean_schemas=True, - id_field="$id", - definitions_key="$defs", - ) + match version: + case JsonSchemaVersion.Draft4: + return cls( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=False, + id_field="id", + definitions_key="definitions", + ) + case JsonSchemaVersion.Draft6 | JsonSchemaVersion.Draft7: + return cls( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=True, + id_field="$id", + definitions_key="definitions", + ) + case JsonSchemaVersion.Draft201909: + return cls( + null_in_type_array=False, + defs_not_definitions=True, + prefix_items=False, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) + case _: + return cls( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) @dataclass(frozen=True) @@ -93,36 +94,43 @@ class OpenAPISchemaFeatures(JsonSchemaFeatures): @classmethod def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures: """Create OpenAPISchemaFeatures from an OpenAPI version.""" - if version == OpenAPIVersion.V30: - # OpenAPI 3.0 does not support boolean schemas (only 3.1+ does) - return cls( - null_in_type_array=False, - defs_not_definitions=False, - prefix_items=False, - boolean_schemas=False, - id_field="$id", - definitions_key="definitions", - nullable_keyword=True, - discriminator_support=True, - ) - # OpenAPI 3.1 or Auto (default to latest features in lenient mode) - return cls( - null_in_type_array=True, - defs_not_definitions=True, - prefix_items=True, - boolean_schemas=True, - id_field="$id", - definitions_key="$defs", - nullable_keyword=False, - discriminator_support=True, - ) - - -# Type variable for SchemaFeatures subclasses + match version: + case OpenAPIVersion.V30: + return cls( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=False, + id_field="$id", + definitions_key="definitions", + nullable_keyword=True, + discriminator_support=True, + ) + case _: + return cls( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + nullable_keyword=False, + discriminator_support=True, + ) + + SchemaFeaturesT = TypeVar("SchemaFeaturesT", bound=JsonSchemaFeatures) +_JSONSCHEMA_VERSION_PATTERNS: dict[str, JsonSchemaVersion] = { + "draft-04": JsonSchemaVersion.Draft4, + "draft-06": JsonSchemaVersion.Draft6, + "draft-07": JsonSchemaVersion.Draft7, + "2019-09": JsonSchemaVersion.Draft201909, + "2020-12": JsonSchemaVersion.Draft202012, +} + -def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: # noqa: PLR0911 +def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: """Detect JSON Schema version from $schema field or heuristics. Detection priority: @@ -139,30 +147,15 @@ def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: # noq Returns: The detected JSON Schema version. """ - schema_url = data.get("$schema", "") - if isinstance(schema_url, str): - if "draft-04" in schema_url: - return JsonSchemaVersion.Draft4 - if "draft-06" in schema_url: - return JsonSchemaVersion.Draft6 - if "draft-07" in schema_url: - return JsonSchemaVersion.Draft7 - if "2019-09" in schema_url: - return JsonSchemaVersion.Draft201909 - if "2020-12" in schema_url: - return JsonSchemaVersion.Draft202012 - - # Heuristic detection (when $schema is missing) + if isinstance(schema_url := data.get("$schema", ""), str): + for pattern, version in _JSONSCHEMA_VERSION_PATTERNS.items(): + if pattern in schema_url: + return version + if "$defs" in data: - # $defs was introduced in Draft 2019-09 - # prefixItems is specific to Draft 2020-12 - if "prefixItems" in data: - return JsonSchemaVersion.Draft202012 - return JsonSchemaVersion.Draft201909 + return JsonSchemaVersion.Draft202012 if "prefixItems" in data else JsonSchemaVersion.Draft201909 if "definitions" in data: return JsonSchemaVersion.Draft7 - - # Fallback: Draft7 is the most widely used return JsonSchemaVersion.Draft7 @@ -175,13 +168,11 @@ def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion: Returns: The detected OpenAPI version. """ - version = data.get("openapi", "") - if isinstance(version, str): + if isinstance(version := data.get("openapi", ""), str): if version.startswith("3.1"): return OpenAPIVersion.V31 if version.startswith("3.0"): return OpenAPIVersion.V30 - # Fallback: 3.1 (latest, best JSON Schema compatibility) return OpenAPIVersion.V31 diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py index 4ac5a7a10..faf5c5804 100644 --- a/tests/parser/test_schema_version.py +++ b/tests/parser/test_schema_version.py @@ -3,9 +3,10 @@ from __future__ import annotations import pytest +from inline_snapshot import snapshot import datamodel_code_generator -from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion +from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion, VersionMode from datamodel_code_generator.parser.schema_version import ( JsonSchemaFeatures, OpenAPISchemaFeatures, @@ -14,221 +15,268 @@ ) -class TestDetectJsonSchemaVersion: - """Tests for detect_jsonschema_version function.""" - - def test_detect_draft4_from_schema(self) -> None: - """Test detection of Draft 4 from $schema field.""" - data = {"$schema": "http://json-schema.org/draft-04/schema#"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft4 - - def test_detect_draft6_from_schema(self) -> None: - """Test detection of Draft 6 from $schema field.""" - data = {"$schema": "http://json-schema.org/draft-06/schema#"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft6 - - def test_detect_draft7_from_schema(self) -> None: - """Test detection of Draft 7 from $schema field.""" - data = {"$schema": "http://json-schema.org/draft-07/schema#"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 - - def test_detect_2019_09_from_schema(self) -> None: - """Test detection of Draft 2019-09 from $schema field.""" - data = {"$schema": "https://json-schema.org/draft/2019-09/schema"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft201909 - - def test_detect_2020_12_from_schema(self) -> None: - """Test detection of Draft 2020-12 from $schema field.""" - data = {"$schema": "https://json-schema.org/draft/2020-12/schema"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft202012 - - def test_detect_from_defs_heuristic_with_prefix_items(self) -> None: - """Test detection using $defs with prefixItems heuristic.""" - data = {"$defs": {"Foo": {"type": "string"}}, "prefixItems": [{"type": "string"}]} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft202012 - - def test_detect_from_defs_heuristic_without_prefix_items(self) -> None: - """Test detection using $defs without prefixItems heuristic.""" - data = {"$defs": {"Foo": {"type": "string"}}} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft201909 - - def test_detect_from_definitions_heuristic(self) -> None: - """Test detection using definitions heuristic.""" - data = {"definitions": {"Foo": {"type": "string"}}} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 - - def test_fallback_to_draft7(self) -> None: - """Test fallback to Draft 7 when no indicators present.""" - data = {"type": "object"} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 - - def test_non_string_schema(self) -> None: - """Test handling of non-string $schema value.""" - data = {"$schema": 123} - assert detect_jsonschema_version(data) == JsonSchemaVersion.Draft7 - - -class TestDetectOpenAPIVersion: - """Tests for detect_openapi_version function.""" - - def test_detect_openapi_30(self) -> None: - """Test detection of OpenAPI 3.0.""" - data = {"openapi": "3.0.0"} - assert detect_openapi_version(data) == OpenAPIVersion.V30 - - def test_detect_openapi_30_patch(self) -> None: - """Test detection of OpenAPI 3.0.x.""" - data = {"openapi": "3.0.3"} - assert detect_openapi_version(data) == OpenAPIVersion.V30 - - def test_detect_openapi_31(self) -> None: - """Test detection of OpenAPI 3.1.""" - data = {"openapi": "3.1.0"} - assert detect_openapi_version(data) == OpenAPIVersion.V31 - - def test_fallback_to_31(self) -> None: - """Test fallback to OpenAPI 3.1 when no version present.""" - data = {"info": {"title": "Test"}} - assert detect_openapi_version(data) == OpenAPIVersion.V31 - - def test_non_string_version(self) -> None: - """Test handling of non-string openapi value.""" - data = {"openapi": 3.0} - assert detect_openapi_version(data) == OpenAPIVersion.V31 - - -class TestJsonSchemaFeatures: - """Tests for JsonSchemaFeatures class.""" - - def test_from_version_draft4(self) -> None: - """Test Draft 4 features.""" - features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft4) - assert features.null_in_type_array is False - assert features.defs_not_definitions is False - assert features.prefix_items is False - assert features.boolean_schemas is False - assert features.id_field == "id" - assert features.definitions_key == "definitions" - - def test_from_version_draft6(self) -> None: - """Test Draft 6 features.""" - features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft6) - assert features.null_in_type_array is False - assert features.defs_not_definitions is False - assert features.prefix_items is False - assert features.boolean_schemas is True - assert features.id_field == "$id" - assert features.definitions_key == "definitions" - - def test_from_version_draft7(self) -> None: - """Test Draft 7 features.""" - features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft7) - assert features.null_in_type_array is False - assert features.defs_not_definitions is False - assert features.prefix_items is False - assert features.boolean_schemas is True - assert features.id_field == "$id" - assert features.definitions_key == "definitions" - - def test_from_version_2019_09(self) -> None: - """Test Draft 2019-09 features.""" - features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft201909) - assert features.null_in_type_array is False - assert features.defs_not_definitions is True - assert features.prefix_items is False - assert features.boolean_schemas is True - assert features.id_field == "$id" - assert features.definitions_key == "$defs" - - def test_from_version_2020_12(self) -> None: - """Test Draft 2020-12 features.""" - features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft202012) - assert features.null_in_type_array is True - assert features.defs_not_definitions is True - assert features.prefix_items is True - assert features.boolean_schemas is True - assert features.id_field == "$id" - assert features.definitions_key == "$defs" - - def test_from_version_auto(self) -> None: - """Test Auto version defaults to latest features.""" - features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Auto) - assert features.null_in_type_array is True - assert features.defs_not_definitions is True - assert features.prefix_items is True - assert features.boolean_schemas is True - - def test_frozen(self) -> None: - """Test that features are immutable.""" - features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft7) - with pytest.raises(AttributeError): - features.null_in_type_array = True # type: ignore[misc] - - -class TestOpenAPISchemaFeatures: - """Tests for OpenAPISchemaFeatures class.""" - - def test_from_openapi_version_v30(self) -> None: - """Test OpenAPI 3.0 features.""" - features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V30) - assert features.nullable_keyword is True - assert features.discriminator_support is True - assert features.boolean_schemas is False # OpenAPI 3.0 does not support boolean schemas - assert features.definitions_key == "definitions" - - def test_from_openapi_version_v31(self) -> None: - """Test OpenAPI 3.1 features.""" - features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V31) - assert features.nullable_keyword is False - assert features.discriminator_support is True - assert features.null_in_type_array is True - assert features.definitions_key == "$defs" - - def test_from_openapi_version_auto(self) -> None: - """Test Auto version defaults to latest features.""" - features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.Auto) - assert features.nullable_keyword is False - assert features.discriminator_support is True - assert features.null_in_type_array is True - - def test_inherits_jsonschema_features(self) -> None: - """Test that OpenAPISchemaFeatures inherits from JsonSchemaFeatures.""" - features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V31) - assert isinstance(features, JsonSchemaFeatures) - assert features.prefix_items is True - - def test_frozen(self) -> None: - """Test that features are immutable.""" - features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V30) - with pytest.raises(AttributeError): - features.nullable_keyword = False # type: ignore[misc] - - -class TestLazyImports: - """Tests for lazy imports from datamodel_code_generator module.""" - - def test_detect_jsonschema_version_lazy_import(self) -> None: - """Test that detect_jsonschema_version can be imported from main module.""" - detect_func = datamodel_code_generator.detect_jsonschema_version - result = detect_func({"$schema": "http://json-schema.org/draft-07/schema#"}) - assert result == JsonSchemaVersion.Draft7 - - def test_detect_openapi_version_lazy_import(self) -> None: - """Test that detect_openapi_version can be imported from main module.""" - detect_func = datamodel_code_generator.detect_openapi_version - result = detect_func({"openapi": "3.1.0"}) - assert result == OpenAPIVersion.V31 - - def test_jsonschema_version_enum_export(self) -> None: - """Test that JsonSchemaVersion is exported from main module.""" - assert datamodel_code_generator.JsonSchemaVersion is JsonSchemaVersion - - def test_openapi_version_enum_export(self) -> None: - """Test that OpenAPIVersion is exported from main module.""" - assert datamodel_code_generator.OpenAPIVersion is OpenAPIVersion - - def test_version_mode_enum_export(self) -> None: - """Test that VersionMode is exported from main module.""" - from datamodel_code_generator.enums import VersionMode - - assert datamodel_code_generator.VersionMode is VersionMode +def test_detect_jsonschema_version_draft4() -> None: + """Test detection of Draft 4 from $schema field.""" + assert detect_jsonschema_version({"$schema": "http://json-schema.org/draft-04/schema#"}) == snapshot( + JsonSchemaVersion.Draft4 + ) + + +def test_detect_jsonschema_version_draft6() -> None: + """Test detection of Draft 6 from $schema field.""" + assert detect_jsonschema_version({"$schema": "http://json-schema.org/draft-06/schema#"}) == snapshot( + JsonSchemaVersion.Draft6 + ) + + +def test_detect_jsonschema_version_draft7() -> None: + """Test detection of Draft 7 from $schema field.""" + assert detect_jsonschema_version({"$schema": "http://json-schema.org/draft-07/schema#"}) == snapshot( + JsonSchemaVersion.Draft7 + ) + + +def test_detect_jsonschema_version_2019_09() -> None: + """Test detection of Draft 2019-09 from $schema field.""" + assert detect_jsonschema_version({"$schema": "https://json-schema.org/draft/2019-09/schema"}) == snapshot( + JsonSchemaVersion.Draft201909 + ) + + +def test_detect_jsonschema_version_2020_12() -> None: + """Test detection of Draft 2020-12 from $schema field.""" + assert detect_jsonschema_version({"$schema": "https://json-schema.org/draft/2020-12/schema"}) == snapshot( + JsonSchemaVersion.Draft202012 + ) + + +def test_detect_jsonschema_version_defs_with_prefix_items() -> None: + """Test detection using $defs with prefixItems heuristic.""" + assert detect_jsonschema_version({"$defs": {"Foo": {"type": "string"}}, "prefixItems": [{"type": "string"}]}) == ( + snapshot(JsonSchemaVersion.Draft202012) + ) + + +def test_detect_jsonschema_version_defs_without_prefix_items() -> None: + """Test detection using $defs without prefixItems heuristic.""" + assert detect_jsonschema_version({"$defs": {"Foo": {"type": "string"}}}) == snapshot(JsonSchemaVersion.Draft201909) + + +def test_detect_jsonschema_version_definitions_heuristic() -> None: + """Test detection using definitions heuristic.""" + assert detect_jsonschema_version({"definitions": {"Foo": {"type": "string"}}}) == snapshot(JsonSchemaVersion.Draft7) + + +def test_detect_jsonschema_version_fallback() -> None: + """Test fallback to Draft 7 when no indicators present.""" + assert detect_jsonschema_version({"type": "object"}) == snapshot(JsonSchemaVersion.Draft7) + + +def test_detect_jsonschema_version_non_string_schema() -> None: + """Test handling of non-string $schema value.""" + assert detect_jsonschema_version({"$schema": 123}) == snapshot(JsonSchemaVersion.Draft7) + + +def test_detect_openapi_version_30() -> None: + """Test detection of OpenAPI 3.0.""" + assert detect_openapi_version({"openapi": "3.0.0"}) == snapshot(OpenAPIVersion.V30) + + +def test_detect_openapi_version_30_patch() -> None: + """Test detection of OpenAPI 3.0.x.""" + assert detect_openapi_version({"openapi": "3.0.3"}) == snapshot(OpenAPIVersion.V30) + + +def test_detect_openapi_version_31() -> None: + """Test detection of OpenAPI 3.1.""" + assert detect_openapi_version({"openapi": "3.1.0"}) == snapshot(OpenAPIVersion.V31) + + +def test_detect_openapi_version_fallback() -> None: + """Test fallback to OpenAPI 3.1 when no version present.""" + assert detect_openapi_version({"info": {"title": "Test"}}) == snapshot(OpenAPIVersion.V31) + + +def test_detect_openapi_version_non_string() -> None: + """Test handling of non-string openapi value.""" + assert detect_openapi_version({"openapi": 3.0}) == snapshot(OpenAPIVersion.V31) + + +def test_jsonschema_features_draft4() -> None: + """Test Draft 4 features.""" + assert JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft4) == snapshot( + JsonSchemaFeatures( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=False, + id_field="id", + definitions_key="definitions", + ) + ) + + +def test_jsonschema_features_draft6() -> None: + """Test Draft 6 features.""" + assert JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft6) == snapshot( + JsonSchemaFeatures( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=True, + id_field="$id", + definitions_key="definitions", + ) + ) + + +def test_jsonschema_features_draft7() -> None: + """Test Draft 7 features.""" + assert JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft7) == snapshot( + JsonSchemaFeatures( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=True, + id_field="$id", + definitions_key="definitions", + ) + ) + + +def test_jsonschema_features_2019_09() -> None: + """Test Draft 2019-09 features.""" + assert JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft201909) == snapshot( + JsonSchemaFeatures( + null_in_type_array=False, + defs_not_definitions=True, + prefix_items=False, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) + ) + + +def test_jsonschema_features_2020_12() -> None: + """Test Draft 2020-12 features.""" + assert JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft202012) == snapshot( + JsonSchemaFeatures( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) + ) + + +def test_jsonschema_features_auto() -> None: + """Test Auto version defaults to latest features.""" + assert JsonSchemaFeatures.from_version(JsonSchemaVersion.Auto) == snapshot( + JsonSchemaFeatures( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) + ) + + +def test_jsonschema_features_frozen() -> None: + """Test that features are immutable.""" + features = JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft7) + with pytest.raises(AttributeError): + features.null_in_type_array = True # type: ignore[misc] + + +def test_openapi_features_v30() -> None: + """Test OpenAPI 3.0 features.""" + assert OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V30) == snapshot( + OpenAPISchemaFeatures( + null_in_type_array=False, + defs_not_definitions=False, + prefix_items=False, + boolean_schemas=False, + id_field="$id", + definitions_key="definitions", + nullable_keyword=True, + discriminator_support=True, + ) + ) + + +def test_openapi_features_v31() -> None: + """Test OpenAPI 3.1 features.""" + assert OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V31) == snapshot( + OpenAPISchemaFeatures( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + nullable_keyword=False, + discriminator_support=True, + ) + ) + + +def test_openapi_features_auto() -> None: + """Test Auto version defaults to latest features.""" + assert OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.Auto) == snapshot( + OpenAPISchemaFeatures( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + nullable_keyword=False, + discriminator_support=True, + ) + ) + + +def test_openapi_features_inherits_jsonschema() -> None: + """Test that OpenAPISchemaFeatures inherits from JsonSchemaFeatures.""" + features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V31) + assert isinstance(features, JsonSchemaFeatures) + assert features.prefix_items == snapshot(True) + + +def test_openapi_features_frozen() -> None: + """Test that features are immutable.""" + features = OpenAPISchemaFeatures.from_openapi_version(OpenAPIVersion.V30) + with pytest.raises(AttributeError): + features.nullable_keyword = False # type: ignore[misc] + + +def test_lazy_import_detect_jsonschema_version() -> None: + """Test that detect_jsonschema_version can be imported from main module.""" + detect_func = datamodel_code_generator.detect_jsonschema_version + assert detect_func({"$schema": "http://json-schema.org/draft-07/schema#"}) == snapshot(JsonSchemaVersion.Draft7) + + +def test_lazy_import_detect_openapi_version() -> None: + """Test that detect_openapi_version can be imported from main module.""" + detect_func = datamodel_code_generator.detect_openapi_version + assert detect_func({"openapi": "3.1.0"}) == snapshot(OpenAPIVersion.V31) + + +def test_lazy_import_jsonschema_version_enum() -> None: + """Test that JsonSchemaVersion is exported from main module.""" + assert datamodel_code_generator.JsonSchemaVersion is JsonSchemaVersion + + +def test_lazy_import_openapi_version_enum() -> None: + """Test that OpenAPIVersion is exported from main module.""" + assert datamodel_code_generator.OpenAPIVersion is OpenAPIVersion + + +def test_lazy_import_version_mode_enum() -> None: + """Test that VersionMode is exported from main module.""" + assert datamodel_code_generator.VersionMode is VersionMode From 19727bb1e69c53504638b453dc84f657fb1ccd6d Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 5 Jan 2026 16:13:24 +0000 Subject: [PATCH 3/5] Use TypeAlias for version patterns dict --- src/datamodel_code_generator/parser/schema_version.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/datamodel_code_generator/parser/schema_version.py b/src/datamodel_code_generator/parser/schema_version.py index 13edd8a2f..6d0f7be19 100644 --- a/src/datamodel_code_generator/parser/schema_version.py +++ b/src/datamodel_code_generator/parser/schema_version.py @@ -7,7 +7,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, TypeVar +from typing import Any, TypeAlias, TypeVar from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion @@ -121,7 +121,9 @@ def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures: SchemaFeaturesT = TypeVar("SchemaFeaturesT", bound=JsonSchemaFeatures) -_JSONSCHEMA_VERSION_PATTERNS: dict[str, JsonSchemaVersion] = { +_JsonSchemaVersionPatterns: TypeAlias = dict[str, JsonSchemaVersion] + +_JSONSCHEMA_VERSION_PATTERNS: _JsonSchemaVersionPatterns = { "draft-04": JsonSchemaVersion.Draft4, "draft-06": JsonSchemaVersion.Draft6, "draft-07": JsonSchemaVersion.Draft7, From 07f46ed4b4f4c4f202295ee4c42a3744c34af930 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 5 Jan 2026 16:13:59 +0000 Subject: [PATCH 4/5] Simplify heuristic to default to Draft 2020-12 --- .../parser/schema_version.py | 2 +- tests/parser/test_schema_version.py | 13 +++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/datamodel_code_generator/parser/schema_version.py b/src/datamodel_code_generator/parser/schema_version.py index 6d0f7be19..460f6f1f0 100644 --- a/src/datamodel_code_generator/parser/schema_version.py +++ b/src/datamodel_code_generator/parser/schema_version.py @@ -155,7 +155,7 @@ def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: return version if "$defs" in data: - return JsonSchemaVersion.Draft202012 if "prefixItems" in data else JsonSchemaVersion.Draft201909 + return JsonSchemaVersion.Draft202012 if "definitions" in data: return JsonSchemaVersion.Draft7 return JsonSchemaVersion.Draft7 diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py index faf5c5804..ea0320c83 100644 --- a/tests/parser/test_schema_version.py +++ b/tests/parser/test_schema_version.py @@ -50,16 +50,9 @@ def test_detect_jsonschema_version_2020_12() -> None: ) -def test_detect_jsonschema_version_defs_with_prefix_items() -> None: - """Test detection using $defs with prefixItems heuristic.""" - assert detect_jsonschema_version({"$defs": {"Foo": {"type": "string"}}, "prefixItems": [{"type": "string"}]}) == ( - snapshot(JsonSchemaVersion.Draft202012) - ) - - -def test_detect_jsonschema_version_defs_without_prefix_items() -> None: - """Test detection using $defs without prefixItems heuristic.""" - assert detect_jsonschema_version({"$defs": {"Foo": {"type": "string"}}}) == snapshot(JsonSchemaVersion.Draft201909) +def test_detect_jsonschema_version_defs_heuristic() -> None: + """Test detection using $defs heuristic defaults to latest.""" + assert detect_jsonschema_version({"$defs": {"Foo": {"type": "string"}}}) == snapshot(JsonSchemaVersion.Draft202012) def test_detect_jsonschema_version_definitions_heuristic() -> None: From ca0cfa0beb56f1b461f1775347c15727359d66b6 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 23:57:11 +0900 Subject: [PATCH 5/5] Add format registry with separation of OpenAPI-specific formats (#2927) * Add format registry with separation of OpenAPI-specific formats * Use snapshot for full format comparison in tests * Integrate format registry with parsers (backward compatible) --- .../parser/jsonschema.py | 36 +++-- .../parser/schema_version.py | 105 ++++++++++++++- tests/parser/test_schema_version.py | 126 ++++++++++++++++++ 3 files changed, 255 insertions(+), 12 deletions(-) diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 20c275e9b..e10ededca 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -509,15 +509,21 @@ def get_ref_type(ref: str) -> JSONReference: return JSONReference.REMOTE -def _get_type(type_: str, format__: str | None = None) -> Types: +def _get_type( + type_: str, + format__: str | None = None, + data_formats: dict[str, dict[str, Types]] | None = None, +) -> Types: """Get the appropriate Types enum for a given JSON Schema type and format.""" - if type_ not in json_schema_data_formats: + if data_formats is None: + data_formats = json_schema_data_formats + if type_ not in data_formats: return Types.any - if (data_formats := json_schema_data_formats[type_].get("default" if format__ is None else format__)) is not None: - return data_formats + if (type_format := data_formats[type_].get("default" if format__ is None else format__)) is not None: + return type_format warn(f"format of {format__!r} not understood for {type_!r} - using default", stacklevel=2) - return json_schema_data_formats[type_]["default"] + return data_formats[type_]["default"] JsonSchemaObject.model_rebuild() @@ -732,21 +738,31 @@ def get_field_extras(self, obj: JsonSchemaObject) -> dict[str, Any]: extras.update(self.default_field_extras) return extras + @cached_property + def _data_formats(self) -> dict[str, dict[str, Types]]: + """Get data format mappings for this parser type. + + Returns all formats for backward compatibility. + OpenAPI-specific formats will be separated in Strict mode (future). + """ + return json_schema_data_formats + def _get_type_with_mappings(self, type_: str, format_: str | None = None) -> Types: """Get the Types enum for a given type and format, applying custom type mappings. Custom mappings from --type-mappings are checked first, then falls back to - the default json_schema_data_formats mappings. + the parser's data format mappings. """ + data_formats = self._data_formats if self.type_mappings and format_ is not None and (type_, format_) in self.type_mappings: target_format = self.type_mappings[type_, format_] - for type_formats in json_schema_data_formats.values(): + for type_formats in data_formats.values(): if target_format in type_formats: return type_formats[target_format] - if target_format in json_schema_data_formats: - return json_schema_data_formats[target_format]["default"] + if target_format in data_formats: + return data_formats[target_format]["default"] - return _get_type(type_, format_) + return _get_type(type_, format_, data_formats) @cached_property def schema_paths(self) -> list[tuple[str, list[str]]]: diff --git a/src/datamodel_code_generator/parser/schema_version.py b/src/datamodel_code_generator/parser/schema_version.py index 460f6f1f0..7e8b71ca0 100644 --- a/src/datamodel_code_generator/parser/schema_version.py +++ b/src/datamodel_code_generator/parser/schema_version.py @@ -1,16 +1,20 @@ """Schema version features and detection utilities. -Provides SchemaFeatures classes for version-dependent feature flags +Provides SchemaFeatures classes for version-dependent feature flags, +format registries for schema-specific data formats, and utility functions for detecting schema versions. """ from __future__ import annotations from dataclasses import dataclass -from typing import Any, TypeAlias, TypeVar +from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion +if TYPE_CHECKING: + from datamodel_code_generator.types import Types + @dataclass(frozen=True) class JsonSchemaFeatures: @@ -178,10 +182,107 @@ def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion: return OpenAPIVersion.V31 +DataFormatMapping: TypeAlias = "dict[str, dict[str, Types]]" + + +def _get_common_data_formats() -> DataFormatMapping: + """Get common data formats valid for both JsonSchema and OpenAPI.""" + from datamodel_code_generator.types import Types # noqa: PLC0415 + + return { + "integer": { + "int32": Types.int32, + "int64": Types.int64, + "default": Types.integer, + "date-time": Types.date_time, + "unix-time": Types.int64, + "unixtime": Types.int64, + }, + "number": { + "float": Types.float, + "double": Types.double, + "decimal": Types.decimal, + "date-time": Types.date_time, + "time": Types.time, + "time-delta": Types.timedelta, + "default": Types.number, + "unixtime": Types.int64, + }, + "string": { + "default": Types.string, + "byte": Types.byte, + "date": Types.date, + "date-time": Types.date_time, + "timestamp with time zone": Types.date_time, + "date-time-local": Types.date_time_local, + "duration": Types.timedelta, + "time": Types.time, + "time-local": Types.time_local, + "path": Types.path, + "email": Types.email, + "idn-email": Types.email, + "uuid": Types.uuid, + "uuid1": Types.uuid1, + "uuid2": Types.uuid2, + "uuid3": Types.uuid3, + "uuid4": Types.uuid4, + "uuid5": Types.uuid5, + "uri": Types.uri, + "uri-reference": Types.string, + "hostname": Types.hostname, + "ipv4": Types.ipv4, + "ipv4-network": Types.ipv4_network, + "ipv6": Types.ipv6, + "ipv6-network": Types.ipv6_network, + "decimal": Types.decimal, + "integer": Types.integer, + "unixtime": Types.int64, + "ulid": Types.ulid, + }, + "boolean": {"default": Types.boolean}, + "object": {"default": Types.object}, + "null": {"default": Types.null}, + "array": {"default": Types.array}, + } + + +def _get_openapi_only_formats() -> DataFormatMapping: + """Get formats specific to OpenAPI (not valid in pure JsonSchema).""" + from datamodel_code_generator.types import Types # noqa: PLC0415 + + return { + "string": { + "binary": Types.binary, + "password": Types.password, + }, + } + + +def get_data_formats(*, is_openapi: bool = False) -> DataFormatMapping: + """Get merged data formats based on schema type. + + Args: + is_openapi: If True, includes OpenAPI-specific formats. + + Returns: + Merged dictionary of data formats. + """ + formats = _get_common_data_formats() + if is_openapi: + for type_key, type_formats in _get_openapi_only_formats().items(): + if type_key in formats: + formats[type_key] = {**formats[type_key], **type_formats} + else: + formats[type_key] = type_formats + return formats + + __all__ = [ + "DataFormatMapping", "JsonSchemaFeatures", "OpenAPISchemaFeatures", "SchemaFeaturesT", "detect_jsonschema_version", "detect_openapi_version", + "get_data_formats", ] diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py index ea0320c83..439c9b44d 100644 --- a/tests/parser/test_schema_version.py +++ b/tests/parser/test_schema_version.py @@ -273,3 +273,129 @@ def test_lazy_import_openapi_version_enum() -> None: def test_lazy_import_version_mode_enum() -> None: """Test that VersionMode is exported from main module.""" assert datamodel_code_generator.VersionMode is VersionMode + + +def test_get_data_formats_jsonschema() -> None: + """Test that JsonSchema formats exclude OpenAPI-specific formats.""" + from datamodel_code_generator.parser.schema_version import get_data_formats + from datamodel_code_generator.types import Types + + assert get_data_formats(is_openapi=False) == snapshot({ + "integer": { + "int32": Types.int32, + "int64": Types.int64, + "default": Types.integer, + "date-time": Types.date_time, + "unix-time": Types.int64, + "unixtime": Types.int64, + }, + "number": { + "float": Types.float, + "double": Types.double, + "decimal": Types.decimal, + "date-time": Types.date_time, + "time": Types.time, + "time-delta": Types.timedelta, + "default": Types.number, + "unixtime": Types.int64, + }, + "string": { + "default": Types.string, + "byte": Types.byte, + "date": Types.date, + "date-time": Types.date_time, + "timestamp with time zone": Types.date_time, + "date-time-local": Types.date_time_local, + "duration": Types.timedelta, + "time": Types.time, + "time-local": Types.time_local, + "path": Types.path, + "email": Types.email, + "idn-email": Types.email, + "uuid": Types.uuid, + "uuid1": Types.uuid1, + "uuid2": Types.uuid2, + "uuid3": Types.uuid3, + "uuid4": Types.uuid4, + "uuid5": Types.uuid5, + "uri": Types.uri, + "uri-reference": Types.string, + "hostname": Types.hostname, + "ipv4": Types.ipv4, + "ipv4-network": Types.ipv4_network, + "ipv6": Types.ipv6, + "ipv6-network": Types.ipv6_network, + "decimal": Types.decimal, + "integer": Types.integer, + "unixtime": Types.int64, + "ulid": Types.ulid, + }, + "boolean": {"default": Types.boolean}, + "object": {"default": Types.object}, + "null": {"default": Types.null}, + "array": {"default": Types.array}, + }) + + +def test_get_data_formats_openapi() -> None: + """Test that OpenAPI formats include OpenAPI-specific formats.""" + from datamodel_code_generator.parser.schema_version import get_data_formats + from datamodel_code_generator.types import Types + + assert get_data_formats(is_openapi=True) == snapshot({ + "integer": { + "int32": Types.int32, + "int64": Types.int64, + "default": Types.integer, + "date-time": Types.date_time, + "unix-time": Types.int64, + "unixtime": Types.int64, + }, + "number": { + "float": Types.float, + "double": Types.double, + "decimal": Types.decimal, + "date-time": Types.date_time, + "time": Types.time, + "time-delta": Types.timedelta, + "default": Types.number, + "unixtime": Types.int64, + }, + "string": { + "default": Types.string, + "byte": Types.byte, + "date": Types.date, + "date-time": Types.date_time, + "timestamp with time zone": Types.date_time, + "date-time-local": Types.date_time_local, + "duration": Types.timedelta, + "time": Types.time, + "time-local": Types.time_local, + "path": Types.path, + "email": Types.email, + "idn-email": Types.email, + "uuid": Types.uuid, + "uuid1": Types.uuid1, + "uuid2": Types.uuid2, + "uuid3": Types.uuid3, + "uuid4": Types.uuid4, + "uuid5": Types.uuid5, + "uri": Types.uri, + "uri-reference": Types.string, + "hostname": Types.hostname, + "ipv4": Types.ipv4, + "ipv4-network": Types.ipv4_network, + "ipv6": Types.ipv6, + "ipv6-network": Types.ipv6_network, + "decimal": Types.decimal, + "integer": Types.integer, + "unixtime": Types.int64, + "ulid": Types.ulid, + "binary": Types.binary, + "password": Types.password, + }, + "boolean": {"default": Types.boolean}, + "object": {"default": Types.object}, + "null": {"default": Types.null}, + "array": {"default": Types.array}, + })