From 811cf13a32121294c0db90c49723fe0dffd061fd Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Mon, 5 Jan 2026 16:48:18 +0000 Subject: [PATCH 01/20] Add schema_features property to parsers for version detection --- .../parser/jsonschema.py | 13 +++++++++++ .../parser/openapi.py | 14 ++++++++++++ tests/parser/test_schema_version.py | 22 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index e10ededca..ecedf2185 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -28,6 +28,7 @@ AllOfClassHierarchy, AllOfMergeMode, InvalidClassNameError, + JsonSchemaVersion, ReadOnlyWriteOnlyModelType, SchemaParseError, YamlValue, @@ -87,6 +88,7 @@ from datamodel_code_generator._types import JSONSchemaParserConfigDict from datamodel_code_generator.config import JSONSchemaParserConfig + from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures def unescape_json_pointer_segment(segment: str) -> str: @@ -769,6 +771,17 @@ def schema_paths(self) -> list[tuple[str, list[str]]]: """Get schema paths for definitions and defs.""" return [(s, s.lstrip("#/").split("/")) for s in self.SCHEMA_PATHS] + @cached_property + def schema_features(self) -> JsonSchemaFeatures: + """Get schema features based on detected version.""" + from datamodel_code_generator.parser.schema_version import ( # noqa: PLC0415 + JsonSchemaFeatures, + detect_jsonschema_version, + ) + + version = detect_jsonschema_version(self.raw_obj) if self.raw_obj else JsonSchemaVersion.Auto + return JsonSchemaFeatures.from_version(version) + @property def root_id(self) -> str | None: """Get the root $id from the model resolver.""" diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 27d59fc55..5f6c57c83 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -11,6 +11,7 @@ from collections import defaultdict from contextlib import nullcontext from enum import Enum +from functools import cached_property from pathlib import Path from re import Pattern from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypeVar, Union @@ -26,6 +27,7 @@ load_data, snooper_to_methods, ) +from datamodel_code_generator.enums import OpenAPIVersion from datamodel_code_generator.parser.base import get_special_path from datamodel_code_generator.parser.jsonschema import ( JsonSchemaObject, @@ -45,6 +47,7 @@ from datamodel_code_generator._types import OpenAPIParserConfigDict from datamodel_code_generator.config import OpenAPIParserConfig from datamodel_code_generator.model import DataModelFieldBase + from datamodel_code_generator.parser.schema_version import OpenAPISchemaFeatures RE_APPLICATION_JSON_PATTERN: Pattern[str] = re.compile(r"^application/.*json$") @@ -167,6 +170,17 @@ class OpenAPIParser(JsonSchemaParser): SCHEMA_PATHS: ClassVar[list[str]] = ["#/components/schemas"] + @cached_property + def schema_features(self) -> OpenAPISchemaFeatures: + """Get schema features based on detected OpenAPI version.""" + from datamodel_code_generator.parser.schema_version import ( # noqa: PLC0415 + OpenAPISchemaFeatures, + detect_openapi_version, + ) + + version = detect_openapi_version(self.raw_obj) if self.raw_obj else OpenAPIVersion.Auto + return OpenAPISchemaFeatures.from_openapi_version(version) + @classmethod def _create_default_config(cls, options: OpenAPIParserConfigDict) -> OpenAPIParserConfig: # ty: ignore """Create an OpenAPIParserConfig from options.""" diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py index 439c9b44d..68f100bb4 100644 --- a/tests/parser/test_schema_version.py +++ b/tests/parser/test_schema_version.py @@ -399,3 +399,25 @@ def test_get_data_formats_openapi() -> None: "null": {"default": Types.null}, "array": {"default": Types.array}, }) + + +def test_jsonschema_parser_schema_features_detection() -> None: + """Test that JsonSchemaParser detects schema version from $schema.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser("") + parser.raw_obj = {"$schema": "http://json-schema.org/draft-07/schema#"} + features = parser.schema_features + assert features.boolean_schemas == snapshot(True) + assert features.definitions_key == snapshot("definitions") + + +def test_openapi_parser_schema_features_detection() -> None: + """Test that OpenAPIParser detects OpenAPI version from openapi field.""" + from datamodel_code_generator.parser.openapi import OpenAPIParser + + parser = OpenAPIParser("") + parser.raw_obj = {"openapi": "3.1.0"} + features = parser.schema_features + assert features.nullable_keyword == snapshot(False) + assert features.null_in_type_array == snapshot(True) From 68ffafad22701a3f0c040d325c676896c2b2cc60 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Wed, 7 Jan 2026 01:08:57 +0900 Subject: [PATCH 02/20] Add version-specific schema processing using schema_features (#2934) * Add --jsonschema-version and --openapi-version CLI options * Add --schema-version and --schema-version-mode CLI options * Regenerate CLI docs * Add version-specific schema processing using schema_features * Implement flag-based behavior control for schema version * Add comprehensive version-specific feature checks with exclusive_as_number flag * Replace getattr with direct config access for schema_version_mode --- docs/cli-reference/base-options.md | 72 +++ docs/cli-reference/index.md | 4 +- docs/cli-reference/quick-reference.md | 4 + src/datamodel_code_generator/__init__.py | 41 +- src/datamodel_code_generator/__main__.py | 5 + .../_types/generate_config_dict.py | 3 + .../_types/parser_config_dicts.py | 7 +- src/datamodel_code_generator/arguments.py | 20 + src/datamodel_code_generator/cli_options.py | 2 + src/datamodel_code_generator/config.py | 9 + .../parser/jsonschema.py | 157 ++++++- .../parser/openapi.py | 36 +- .../parser/schema_version.py | 16 +- .../expected/main/input_model/config_class.py | 5 + .../test_public_api_signature_baseline.py | 3 + tests/parser/test_schema_version.py | 410 +++++++++++++++++- 16 files changed, 770 insertions(+), 24 deletions(-) diff --git a/docs/cli-reference/base-options.md b/docs/cli-reference/base-options.md index 12be41743..1f633a906 100644 --- a/docs/cli-reference/base-options.md +++ b/docs/cli-reference/base-options.md @@ -10,6 +10,8 @@ | [`--input-model`](#input-model) | Import a Python type or dict schema from a module. | | [`--input-model-ref-strategy`](#input-model-ref-strategy) | Strategy for referenced types when using --input-model. | | [`--output`](#output) | Specify the destination path for generated Python code. | +| [`--schema-version`](#schema-version) | Schema version to use for parsing. | +| [`--schema-version-mode`](#schema-version-mode) | Schema version validation mode. | | [`--url`](#url) | Fetch schema from URL with custom HTTP headers. | --- @@ -326,6 +328,76 @@ is written to stdout. --- +## `--schema-version` {#schema-version} + +Schema version to use for parsing. + +The `--schema-version` option specifies the schema version to use instead of auto-detection. +Valid values depend on input type: JsonSchema (draft-04, draft-06, draft-07, 2019-09, 2020-12) +or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field). + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --schema-version draft-07 # (1)! + ``` + + 1. :material-arrow-left: `--schema-version` - the option documented here + +??? example "Examples" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": {"s": {"type": ["string"]}}, + "required": ["s"] + } + ``` + + **Output:** + + > **Error:** File not found: jsonschema/simple_string.py + +--- + +## `--schema-version-mode` {#schema-version-mode} + +Schema version validation mode. + +The `--schema-version-mode` option controls how schema version validation is performed. +'lenient' (default): accept all features regardless of version. +'strict': warn on features outside the declared/detected version. + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --schema-version-mode lenient # (1)! + ``` + + 1. :material-arrow-left: `--schema-version-mode` - the option documented here + +??? example "Examples" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": {"s": {"type": ["string"]}}, + "required": ["s"] + } + ``` + + **Output:** + + > **Error:** File not found: jsonschema/simple_string.py + +--- + ## `--url` {#url} Fetch schema from URL with custom HTTP headers. diff --git a/docs/cli-reference/index.md b/docs/cli-reference/index.md index cd96c3d34..0f7c2ef64 100644 --- a/docs/cli-reference/index.md +++ b/docs/cli-reference/index.md @@ -8,7 +8,7 @@ This documentation is auto-generated from test cases. | Category | Options | Description | |----------|---------|-------------| -| 📁 [Base Options](base-options.md) | 7 | Input/output configuration | +| 📁 [Base Options](base-options.md) | 9 | Input/output configuration | | 🔧 [Typing Customization](typing-customization.md) | 27 | Type annotation and import behavior | | 🏷️ [Field Customization](field-customization.md) | 24 | Field naming and docstring behavior | | 🏗️ [Model Customization](model-customization.md) | 39 | Model generation behavior | @@ -161,6 +161,8 @@ This documentation is auto-generated from test cases. ### S {#s} +- [`--schema-version`](base-options.md#schema-version) +- [`--schema-version-mode`](base-options.md#schema-version-mode) - [`--set-default-enum-member`](field-customization.md#set-default-enum-member) - [`--shared-module-name`](general-options.md#shared-module-name) - [`--skip-root-model`](model-customization.md#skip-root-model) diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index d9e57eaf9..d4f4c6499 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -22,6 +22,8 @@ datamodel-codegen [OPTIONS] | [`--input-model`](base-options.md#input-model) | Import a Python type or dict schema from a module. | | [`--input-model-ref-strategy`](base-options.md#input-model-ref-strategy) | Strategy for referenced types when using --input-model. | | [`--output`](base-options.md#output) | Specify the destination path for generated Python code. | +| [`--schema-version`](base-options.md#schema-version) | Schema version to use for parsing. | +| [`--schema-version-mode`](base-options.md#schema-version-mode) | Schema version validation mode. | | [`--url`](base-options.md#url) | Fetch schema from URL with custom HTTP headers. | ### 🔧 Typing Customization @@ -299,6 +301,8 @@ All options sorted alphabetically: - [`--remove-special-field-name-prefix`](field-customization.md#remove-special-field-name-prefix) - Remove the special prefix from field names. - [`--reuse-model`](model-customization.md#reuse-model) - Reuse identical model definitions instead of generating dupl... - [`--reuse-scope`](model-customization.md#reuse-scope) - Scope for model reuse detection (root or tree). +- [`--schema-version`](base-options.md#schema-version) - Schema version to use for parsing. +- [`--schema-version-mode`](base-options.md#schema-version-mode) - Schema version validation mode. - [`--set-default-enum-member`](field-customization.md#set-default-enum-member) - Set the first enum member as the default value for enum fiel... - [`--shared-module-name`](general-options.md#shared-module-name) - Customize the name of the shared module for deduplicated mod... - [`--skip-root-model`](model-customization.md#skip-root-model) - Skip generation of root model when schema contains nested de... diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py index e63c9840e..23a79bf21 100644 --- a/src/datamodel_code_generator/__init__.py +++ b/src/datamodel_code_generator/__init__.py @@ -65,7 +65,12 @@ from datamodel_code_generator.parser import DefaultPutDict, LiteralType if TYPE_CHECKING: - from datamodel_code_generator._types import GraphQLParserConfigDict, OpenAPIParserConfigDict, ParserConfigDict + from datamodel_code_generator._types import ( + GraphQLParserConfigDict, + JSONSchemaParserConfigDict, + OpenAPIParserConfigDict, + ParserConfigDict, + ) from datamodel_code_generator._types.generate_config_dict import GenerateConfigDict from datamodel_code_generator.config import GenerateConfig, ParserConfig @@ -456,7 +461,10 @@ def _build_module_content( def _create_parser_config( config_class: type[_ConfigT], generate_config: GenerateConfig, - additional_options: ParserConfigDict | OpenAPIParserConfigDict | GraphQLParserConfigDict, + additional_options: ParserConfigDict + | JSONSchemaParserConfigDict + | OpenAPIParserConfigDict + | GraphQLParserConfigDict, ) -> _ConfigT: """Create a parser config from GenerateConfig with additional options. @@ -735,6 +743,28 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]: ), } + # Convert schema_version string to appropriate enum based on input type + jsonschema_version: JsonSchemaVersion | None = None + openapi_version: OpenAPIVersion | None = None + if config.schema_version and config.schema_version != "auto": + if input_file_type == InputFileType.OpenAPI: + try: + openapi_version = OpenAPIVersion(config.schema_version) + except ValueError: + valid = [v.value for v in OpenAPIVersion] + msg = f"Invalid OpenAPI version: {config.schema_version}. Valid values: {valid}" + raise Error(msg) from None + elif input_file_type == InputFileType.GraphQL: + msg = f"--schema-version is not supported for {input_file_type.value}" + raise Error(msg) + else: + try: + jsonschema_version = JsonSchemaVersion(config.schema_version) + except ValueError: + valid = [v.value for v in JsonSchemaVersion] + msg = f"Invalid JSON Schema version: {config.schema_version}. Valid values: {valid}" + raise Error(msg) from None + if input_file_type == InputFileType.OpenAPI: from datamodel_code_generator.parser.openapi import OpenAPIParser # noqa: PLC0415 @@ -743,6 +773,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]: "include_path_parameters": config.include_path_parameters, "use_status_code_in_response_name": config.use_status_code_in_response_name, "openapi_include_paths": config.openapi_include_paths, + "openapi_version": openapi_version, **additional_options, } parser_config = _create_parser_config(OpenAPIParserConfig, config, openapi_additional_options) @@ -760,7 +791,11 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]: else: from datamodel_code_generator.parser.jsonschema import JsonSchemaParser # noqa: PLC0415 - parser_config = _create_parser_config(JSONSchemaParserConfig, config, additional_options) + jsonschema_additional_options: JSONSchemaParserConfigDict = { + "jsonschema_version": jsonschema_version, + **additional_options, + } + parser_config = _create_parser_config(JSONSchemaParserConfig, config, jsonschema_additional_options) parser = JsonSchemaParser(source=source, config=parser_config) # ty: ignore with chdir(config.output): diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index 6039cb9c7..e7d4c68ba 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -69,6 +69,7 @@ ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, + VersionMode, enable_debug_message, generate, ) @@ -620,6 +621,8 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) -> module_split_mode: Optional[ModuleSplitMode] = None # noqa: UP045 watch: bool = False watch_delay: float = 0.5 + schema_version: Optional[str] = None # noqa: UP045 + schema_version_mode: Optional[VersionMode] = None # noqa: UP045 def merge_args(self, args: Namespace) -> None: """Merge command-line arguments into config.""" @@ -1063,6 +1066,8 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917 module_split_mode=config.module_split_mode, validators=validators, default_value_overrides=default_value_overrides, + schema_version=config.schema_version, + schema_version_mode=config.schema_version_mode, ) if output is None and result is not None: # pragma: no cover diff --git a/src/datamodel_code_generator/_types/generate_config_dict.py b/src/datamodel_code_generator/_types/generate_config_dict.py index d997a3344..da3c652e3 100644 --- a/src/datamodel_code_generator/_types/generate_config_dict.py +++ b/src/datamodel_code_generator/_types/generate_config_dict.py @@ -32,6 +32,7 @@ StrictTypes, TargetPydanticVersion, UnionMode, + VersionMode, ) from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion from datamodel_code_generator.parser import LiteralType @@ -168,6 +169,8 @@ class GenerateConfigDict(TypedDict, closed=True): field_type_collision_strategy: NotRequired[FieldTypeCollisionStrategy | None] module_split_mode: NotRequired[ModuleSplitMode | None] default_value_overrides: NotRequired[Mapping[str, Any] | None] + schema_version: NotRequired[str | None] + schema_version_mode: NotRequired[VersionMode | None] class ValidatorDefinition(TypedDict): diff --git a/src/datamodel_code_generator/_types/parser_config_dicts.py b/src/datamodel_code_generator/_types/parser_config_dicts.py index ff4d71dd7..e61597c25 100644 --- a/src/datamodel_code_generator/_types/parser_config_dicts.py +++ b/src/datamodel_code_generator/_types/parser_config_dicts.py @@ -19,12 +19,15 @@ CollapseRootModelsNameStrategy, DataclassArguments, FieldTypeCollisionStrategy, + JsonSchemaVersion, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, StrictTypes, TargetPydanticVersion, + VersionMode, ) from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion from datamodel_code_generator.model.base import DataModel, DataModelFieldBase @@ -167,7 +170,8 @@ class GraphQLParserConfigDict(ParserConfigDict, closed=True): class JSONSchemaParserConfigDict(ParserConfigDict): - pass + jsonschema_version: NotRequired[JsonSchemaVersion | None] + schema_version_mode: NotRequired[VersionMode | None] class OpenAPIParserConfigDict(JSONSchemaParserConfigDict, closed=True): @@ -175,6 +179,7 @@ class OpenAPIParserConfigDict(JSONSchemaParserConfigDict, closed=True): include_path_parameters: NotRequired[bool] use_status_code_in_response_name: NotRequired[bool] openapi_include_paths: NotRequired[list[str] | None] + openapi_version: NotRequired[OpenAPIVersion | None] ModelDict: TypeAlias = ParserConfigDict | GraphQLParserConfigDict | JSONSchemaParserConfigDict | OpenAPIParserConfigDict diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py index bdf680fb9..67dcc0ba9 100644 --- a/src/datamodel_code_generator/arguments.py +++ b/src/datamodel_code_generator/arguments.py @@ -34,6 +34,7 @@ StrictTypes, TargetPydanticVersion, UnionMode, + VersionMode, ) from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion from datamodel_code_generator.parser import LiteralType @@ -984,6 +985,25 @@ def start_section(self, heading: str | None) -> None: action="store_true", default=None, ) +# ====================================================================================== +# Schema version options (for both JSON Schema and OpenAPI) +# ====================================================================================== +base_options.add_argument( + "--schema-version", + help="Schema version. Valid values depend on input type: " + "JsonSchema: auto, draft-04, draft-06, draft-07, 2019-09, 2020-12. " + "OpenAPI: auto, 3.0, 3.1. " + "(default: auto - detected from $schema or openapi field)", + default=None, +) +base_options.add_argument( + "--schema-version-mode", + help="Schema version validation mode. " + "'lenient': accept all features regardless of version (default). " + "'strict': warn on features outside declared/detected version.", + choices=[m.value for m in VersionMode], + default=None, +) # ====================================================================================== # Options specific to GraphQL input schemas diff --git a/src/datamodel_code_generator/cli_options.py b/src/datamodel_code_generator/cli_options.py index 570cf73b0..ef993e0be 100644 --- a/src/datamodel_code_generator/cli_options.py +++ b/src/datamodel_code_generator/cli_options.py @@ -70,6 +70,8 @@ class CLIOptionMeta: "--input-model-ref-strategy": CLIOptionMeta(name="--input-model-ref-strategy", category=OptionCategory.BASE), "--input-file-type": CLIOptionMeta(name="--input-file-type", category=OptionCategory.BASE), "--encoding": CLIOptionMeta(name="--encoding", category=OptionCategory.BASE), + "--schema-version": CLIOptionMeta(name="--schema-version", category=OptionCategory.BASE), + "--schema-version-mode": CLIOptionMeta(name="--schema-version-mode", category=OptionCategory.BASE), # ========================================================================== # Model Customization # ========================================================================== diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index ac37fd6dc..c91d51c52 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -22,12 +22,15 @@ FieldTypeCollisionStrategy, GraphQLScope, InputFileType, + JsonSchemaVersion, ModuleSplitMode, NamingStrategy, OpenAPIScope, + OpenAPIVersion, ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, + VersionMode, ) from datamodel_code_generator.format import ( DateClassType, @@ -205,6 +208,8 @@ class Config: field_type_collision_strategy: FieldTypeCollisionStrategy | None = None module_split_mode: ModuleSplitMode | None = None default_value_overrides: Mapping[str, Any] | None = None + schema_version: str | None = None + schema_version_mode: VersionMode | None = None class ParserConfig(BaseModel): @@ -350,6 +355,9 @@ class GraphQLParserConfig(ParserConfig): class JSONSchemaParserConfig(ParserConfig): """Configuration model for JsonSchemaParser.__init__().""" + jsonschema_version: JsonSchemaVersion | None = None + schema_version_mode: VersionMode | None = None + class OpenAPIParserConfig(JSONSchemaParserConfig): """Configuration model for OpenAPIParser.__init__().""" @@ -358,6 +366,7 @@ class OpenAPIParserConfig(JSONSchemaParserConfig): include_path_parameters: bool = False use_status_code_in_response_name: bool = False openapi_include_paths: list[str] | None = None + openapi_version: OpenAPIVersion | None = None class ParseConfig(BaseModel): diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index ecedf2185..32312cf67 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -31,6 +31,7 @@ JsonSchemaVersion, ReadOnlyWriteOnlyModelType, SchemaParseError, + VersionMode, YamlValue, load_data, load_data_from_path, @@ -192,7 +193,12 @@ class JSONReference(_enum.Enum): class Discriminator(BaseModel): - """Represent OpenAPI discriminator object.""" + """Represent OpenAPI discriminator object. + + This is an OpenAPI-specific concept for supporting polymorphism. + It identifies which schema applies based on a property value. + Kept in jsonschema.py to avoid circular imports with openapi.py. + """ propertyName: str # noqa: N815 mapping: Optional[dict[str, str]] = None # noqa: UP045 @@ -517,7 +523,7 @@ def _get_type( data_formats: dict[str, dict[str, Types]] | None = None, ) -> Types: """Get the appropriate Types enum for a given JSON Schema type and format.""" - if data_formats is None: + if data_formats is None: # pragma: no cover data_formats = json_schema_data_formats if type_ not in data_formats: return Types.any @@ -664,7 +670,7 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig"]): }) @classmethod - def _create_default_config(cls, options: JSONSchemaParserConfigDict) -> JSONSchemaParserConfig: + def _create_default_config(cls, options: JSONSchemaParserConfigDict) -> JSONSchemaParserConfig: # type: ignore[override] """Create a JSONSchemaParserConfig from options.""" from datamodel_code_generator import types as types_module # noqa: PLC0415 from datamodel_code_generator.config import JSONSchemaParserConfig # noqa: PLC0415 @@ -768,17 +774,43 @@ def _get_type_with_mappings(self, type_: str, format_: str | None = None) -> Typ @cached_property def schema_paths(self) -> list[tuple[str, list[str]]]: - """Get schema paths for definitions and defs.""" - return [(s, s.lstrip("#/").split("/")) for s in self.SCHEMA_PATHS] + """Get schema paths for definitions and defs. + + For JsonSchema, uses schema_features.definitions_key to determine + the primary path, with fallback to the alternative in Lenient mode. + OpenAPI subclass uses its own SCHEMA_PATHS (#/components/schemas). + """ + # OpenAPI and other subclasses use their own SCHEMA_PATHS + if self.SCHEMA_PATHS != ["#/definitions", "#/$defs"]: + return [(s, s.lstrip("#/").split("/")) for s in self.SCHEMA_PATHS] + + # JsonSchema: use definitions_key from schema_features + primary_key = self.schema_features.definitions_key + primary_path = f"#/{primary_key}" + fallback_key = "$defs" if primary_key == "definitions" else "definitions" + fallback_path = f"#/{fallback_key}" + + # Strict mode: only use version-specific path + if self.config.schema_version_mode == VersionMode.Strict: + return [(str(primary_path), [str(primary_key)])] + + # Lenient mode (default): check both paths, primary first + return [ + (str(primary_path), [str(primary_key)]), + (str(fallback_path), [str(fallback_key)]), + ] @cached_property def schema_features(self) -> JsonSchemaFeatures: - """Get schema features based on detected version.""" + """Get schema features based on config or detected version.""" from datamodel_code_generator.parser.schema_version import ( # noqa: PLC0415 JsonSchemaFeatures, detect_jsonschema_version, ) + config_version = getattr(self.config, "jsonschema_version", None) + if config_version is not None and config_version != JsonSchemaVersion.Auto: + return JsonSchemaFeatures.from_version(config_version) version = detect_jsonschema_version(self.raw_obj) if self.raw_obj else JsonSchemaVersion.Auto return JsonSchemaFeatures.from_version(version) @@ -2918,6 +2950,9 @@ def parse_array_fields( # noqa: PLR0912 singular_name: bool = True, # noqa: FBT001, FBT002 ) -> DataModelFieldBase: """Parse array schema into a data model field with list type.""" + # Strict mode: check for version-specific array features + self._check_array_version_features(obj, path) + if self.force_optional_for_required_fields: required: bool = False nullable: Optional[bool] = None # noqa: UP045 @@ -3589,9 +3624,15 @@ def parse_id(self, obj: JsonSchemaObject, path: list[str]) -> None: @contextmanager def root_id_context(self, root_raw: dict[str, Any]) -> Generator[None, None, None]: - """Context manager to temporarily set the root $id during parsing.""" + """Context manager to temporarily set the root $id during parsing. + + Uses schema_features.id_field to support both "id" (Draft 4) and "$id" (Draft 6+). + Falls back to checking both fields for lenient compatibility. + """ previous_root_id = self.root_id - self.root_id = root_raw.get("$id") or None + # Try version-specific field first, then fallback to alternative for compatibility + id_field = self.schema_features.id_field + self.root_id = root_raw.get(id_field) or root_raw.get("$id") or root_raw.get("id") or None yield self.root_id = previous_root_id @@ -3622,9 +3663,109 @@ def parse_raw_obj( if isinstance(raw, dict) and "x-python-import" in raw: self._handle_python_import(name, path) return + + # Strict mode: check for version-specific features before validation + self._check_version_specific_features(raw, path) + obj = self._validate_schema_object(raw, path) self.parse_obj(name, obj, path) + def _check_version_specific_features( + self, + raw: dict[str, YamlValue] | YamlValue, + path: list[str], + ) -> None: + """Check for version-specific features and warn in Strict mode. + + This method checks the raw schema data before Pydantic validation + to detect features that may not be valid for the declared version. + """ + if self.config.schema_version_mode != VersionMode.Strict: + return + + # Check boolean schemas (Draft 6+) + if isinstance(raw, bool): + if not self.schema_features.boolean_schemas: + version_name = "Draft 4" if self.schema_features.id_field == "id" else "this version" + warn( + f"Boolean schemas are not supported in {version_name}. Schema path: {'/'.join(path)}", + stacklevel=3, + ) + return + + if not isinstance(raw, dict): + return + + # Check null in type array (Draft 2020-12 / OpenAPI 3.1+) + type_value = raw.get("type") + if isinstance(type_value, list) and "null" in type_value and not self.schema_features.null_in_type_array: + warn( + 'null in type array (e.g., type: ["string", "null"]) is not supported ' + f"in this schema version. Use nullable: true instead. Schema path: {'/'.join(path)}", + stacklevel=3, + ) + + # Check exclusive min/max format (Draft 4 uses boolean, Draft 6+ uses number) + exclusive_min = raw.get("exclusiveMinimum") + exclusive_max = raw.get("exclusiveMaximum") + if self.schema_features.exclusive_as_number: + # Draft 6+: should be numeric, not boolean + if isinstance(exclusive_min, bool): + warn( + f"exclusiveMinimum as boolean is Draft 4 style, but schema version uses numeric style. " + f"Schema path: {'/'.join(path)}", + stacklevel=3, + ) + if isinstance(exclusive_max, bool): + warn( + f"exclusiveMaximum as boolean is Draft 4 style, but schema version uses numeric style. " + f"Schema path: {'/'.join(path)}", + stacklevel=3, + ) + else: + # Draft 4: should be boolean, not numeric + if exclusive_min is not None and not isinstance(exclusive_min, bool): + warn( + f"exclusiveMinimum as number is Draft 6+ style, but schema version is Draft 4. " + f"Schema path: {'/'.join(path)}", + stacklevel=3, + ) + if exclusive_max is not None and not isinstance(exclusive_max, bool): + warn( + f"exclusiveMaximum as number is Draft 6+ style, but schema version is Draft 4. " + f"Schema path: {'/'.join(path)}", + stacklevel=3, + ) + + def _check_array_version_features( + self, + obj: JsonSchemaObject, + path: list[str], + ) -> None: + """Check for version-specific array features and warn in Strict mode. + + Warns when prefixItems is used in versions that don't support it, + or when items as array (tuple style) is used in Draft 2020-12+. + """ + if self.config.schema_version_mode != VersionMode.Strict: + return + + # Check prefixItems usage (Draft 2020-12+ only) + if obj.prefixItems is not None and not self.schema_features.prefix_items: + warn( + f"prefixItems is not supported in this schema version. " + f"Use items as array for tuple validation. Schema path: {'/'.join(path)}", + stacklevel=4, + ) + + # Check items as array usage (deprecated in Draft 2020-12) + if isinstance(obj.items, list) and self.schema_features.prefix_items: + warn( + f"items as array (tuple validation) is deprecated in Draft 2020-12. " + f"Use prefixItems instead. Schema path: {'/'.join(path)}", + stacklevel=4, + ) + def _handle_python_import( self, name: str, diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 5f6c57c83..4b86f8908 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -27,7 +27,7 @@ load_data, snooper_to_methods, ) -from datamodel_code_generator.enums import OpenAPIVersion +from datamodel_code_generator.enums import OpenAPIVersion, VersionMode from datamodel_code_generator.parser.base import get_special_path from datamodel_code_generator.parser.jsonschema import ( JsonSchemaObject, @@ -172,12 +172,15 @@ class OpenAPIParser(JsonSchemaParser): @cached_property def schema_features(self) -> OpenAPISchemaFeatures: - """Get schema features based on detected OpenAPI version.""" + """Get schema features based on config or detected OpenAPI version.""" from datamodel_code_generator.parser.schema_version import ( # noqa: PLC0415 OpenAPISchemaFeatures, detect_openapi_version, ) + config_version = getattr(self.config, "openapi_version", None) + if config_version is not None and config_version != OpenAPIVersion.Auto: + return OpenAPISchemaFeatures.from_openapi_version(config_version) version = detect_openapi_version(self.raw_obj) if self.raw_obj else OpenAPIVersion.Auto return OpenAPISchemaFeatures.from_openapi_version(version) @@ -239,13 +242,28 @@ def get_ref_model(self, ref: str) -> dict[str, Any]: return get_model_by_path(ref_body, ref_path.split("/")[1:]) def get_data_type(self, obj: JsonSchemaObject) -> DataType: - """Get data type from JSON schema object, handling OpenAPI nullable semantics.""" - # OpenAPI 3.0 doesn't allow `null` in the `type` field and list of types - # 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 - if obj.nullable and self.strict_nullable and isinstance(obj.type, str): - obj.type = [obj.type, "null"] + """Get data type from JSON schema object, handling OpenAPI nullable semantics. + + Uses schema_features.nullable_keyword to handle version differences: + - OpenAPI 3.0: nullable: true is valid, convert to type array when strict_nullable + - OpenAPI 3.1: nullable is deprecated, use type: ["string", "null"] instead + """ + if obj.nullable: + if self.schema_features.nullable_keyword: + # OpenAPI 3.0: nullable: true is the standard way + if self.strict_nullable and isinstance(obj.type, str): + obj.type = [obj.type, "null"] + else: + # OpenAPI 3.1+: nullable is deprecated, still process but warn in Strict mode + if self.config.schema_version_mode == VersionMode.Strict: + warn( + 'nullable keyword is deprecated in OpenAPI 3.1, use type: ["string", "null"] instead', + DeprecationWarning, + stacklevel=2, + ) + # Still convert to type array for compatibility + if self.strict_nullable and isinstance(obj.type, str): + obj.type = [obj.type, "null"] return super().get_data_type(obj) diff --git a/src/datamodel_code_generator/parser/schema_version.py b/src/datamodel_code_generator/parser/schema_version.py index 7e8b71ca0..96d9156b4 100644 --- a/src/datamodel_code_generator/parser/schema_version.py +++ b/src/datamodel_code_generator/parser/schema_version.py @@ -30,6 +30,7 @@ class JsonSchemaFeatures: 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"). + exclusive_as_number: Draft 6+ uses numeric exclusiveMin/Max (Draft 4 uses boolean). """ null_in_type_array: bool @@ -38,6 +39,7 @@ class JsonSchemaFeatures: boolean_schemas: bool id_field: str definitions_key: str + exclusive_as_number: bool @classmethod def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: @@ -51,6 +53,7 @@ def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: boolean_schemas=False, id_field="id", definitions_key="definitions", + exclusive_as_number=False, ) case JsonSchemaVersion.Draft6 | JsonSchemaVersion.Draft7: return cls( @@ -60,6 +63,7 @@ def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: boolean_schemas=True, id_field="$id", definitions_key="definitions", + exclusive_as_number=True, ) case JsonSchemaVersion.Draft201909: return cls( @@ -69,6 +73,7 @@ def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, ) case _: return cls( @@ -78,6 +83,7 @@ def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, ) @@ -100,6 +106,8 @@ def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures: """Create OpenAPISchemaFeatures from an OpenAPI version.""" match version: case OpenAPIVersion.V30: + # OpenAPI 3.0 schema dialect inherits JSON Schema Draft 4 semantics + # where exclusiveMinimum/Maximum are boolean values return cls( null_in_type_array=False, defs_not_definitions=False, @@ -107,6 +115,7 @@ def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures: boolean_schemas=False, id_field="$id", definitions_key="definitions", + exclusive_as_number=False, nullable_keyword=True, discriminator_support=True, ) @@ -118,6 +127,7 @@ def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, nullable_keyword=False, discriminator_support=True, ) @@ -158,6 +168,10 @@ def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: if pattern in schema_url: return version + # Heuristic detection based on keywords + # $defs was introduced in Draft 2019-09, but Draft 2020-12 also uses it. + # Since 2020-12 is a superset of 2019-09, default to 2020-12 when $defs is present + # to avoid false warnings in Strict mode for features valid in both versions. if "$defs" in data: return JsonSchemaVersion.Draft202012 if "definitions" in data: @@ -272,7 +286,7 @@ def get_data_formats(*, is_openapi: bool = False) -> DataFormatMapping: 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: + else: # pragma: no cover formats[type_key] = type_formats return formats diff --git a/tests/data/expected/main/input_model/config_class.py b/tests/data/expected/main/input_model/config_class.py index 184b3d79f..285b094a4 100644 --- a/tests/data/expected/main/input_model/config_class.py +++ b/tests/data/expected/main/input_model/config_class.py @@ -114,6 +114,9 @@ class DataclassArguments(TypedDict, closed=True): ValidatorMode: TypeAlias = Literal['before', 'after', 'wrap', 'plain'] +VersionMode: TypeAlias = Literal['lenient', 'strict'] + + class GenerateConfig(TypedDict, closed=True): input_filename: NotRequired[str | None] input_file_type: NotRequired[InputFileType] @@ -246,6 +249,8 @@ class GenerateConfig(TypedDict, closed=True): field_type_collision_strategy: NotRequired[FieldTypeCollisionStrategy | None] module_split_mode: NotRequired[ModuleSplitMode | None] default_value_overrides: NotRequired[Mapping[str, Any] | None] + schema_version: NotRequired[str | None] + schema_version_mode: NotRequired[VersionMode | None] class ValidatorDefinition(TypedDict): diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 3c0327259..9d2c7efee 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -28,6 +28,7 @@ ReadOnlyWriteOnlyModelType, ReuseScope, TargetPydanticVersion, + VersionMode, ) from datamodel_code_generator.format import PythonVersionMin from datamodel_code_generator.model import DataModel, DataModelFieldBase @@ -187,6 +188,8 @@ def _baseline_generate( all_exports_collision_strategy: AllExportsCollisionStrategy | None = None, field_type_collision_strategy: FieldTypeCollisionStrategy | None = None, module_split_mode: ModuleSplitMode | None = None, + schema_version: str | None = None, + schema_version_mode: VersionMode | None = None, ) -> str | object | None: raise NotImplementedError diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py index 68f100bb4..2fddf861c 100644 --- a/tests/parser/test_schema_version.py +++ b/tests/parser/test_schema_version.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pathlib import Path + import pytest from inline_snapshot import snapshot @@ -14,6 +16,9 @@ detect_openapi_version, ) +# Path to test data +JSON_SCHEMA_DATA_PATH = Path(__file__).parent.parent / "data" / "jsonschema" + def test_detect_jsonschema_version_draft4() -> None: """Test detection of Draft 4 from $schema field.""" @@ -51,7 +56,11 @@ def test_detect_jsonschema_version_2020_12() -> None: def test_detect_jsonschema_version_defs_heuristic() -> None: - """Test detection using $defs heuristic defaults to latest.""" + """Test detection using $defs heuristic. + + $defs was introduced in Draft 2019-09, but Draft 2020-12 also uses it. + Since 2020-12 is a superset, default to 2020-12 to avoid false warnings. + """ assert detect_jsonschema_version({"$defs": {"Foo": {"type": "string"}}}) == snapshot(JsonSchemaVersion.Draft202012) @@ -105,6 +114,7 @@ def test_jsonschema_features_draft4() -> None: boolean_schemas=False, id_field="id", definitions_key="definitions", + exclusive_as_number=False, ) ) @@ -119,6 +129,7 @@ def test_jsonschema_features_draft6() -> None: boolean_schemas=True, id_field="$id", definitions_key="definitions", + exclusive_as_number=True, ) ) @@ -133,6 +144,7 @@ def test_jsonschema_features_draft7() -> None: boolean_schemas=True, id_field="$id", definitions_key="definitions", + exclusive_as_number=True, ) ) @@ -147,6 +159,7 @@ def test_jsonschema_features_2019_09() -> None: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, ) ) @@ -161,6 +174,7 @@ def test_jsonschema_features_2020_12() -> None: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, ) ) @@ -175,6 +189,7 @@ def test_jsonschema_features_auto() -> None: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, ) ) @@ -196,6 +211,7 @@ def test_openapi_features_v30() -> None: boolean_schemas=False, id_field="$id", definitions_key="definitions", + exclusive_as_number=False, nullable_keyword=True, discriminator_support=True, ) @@ -212,6 +228,7 @@ def test_openapi_features_v31() -> None: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, nullable_keyword=False, discriminator_support=True, ) @@ -228,6 +245,7 @@ def test_openapi_features_auto() -> None: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, nullable_keyword=False, discriminator_support=True, ) @@ -421,3 +439,393 @@ def test_openapi_parser_schema_features_detection() -> None: features = parser.schema_features assert features.nullable_keyword == snapshot(False) assert features.null_in_type_array == snapshot(True) + + +def test_jsonschema_parser_config_version_override() -> None: + """Test that JsonSchemaParser uses config version over auto-detection.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser("", jsonschema_version=JsonSchemaVersion.Draft4) + parser.raw_obj = {"$schema": "http://json-schema.org/draft-07/schema#"} + features = parser.schema_features + assert features.id_field == snapshot("id") + assert features.boolean_schemas == snapshot(False) + + +def test_openapi_parser_config_version_override() -> None: + """Test that OpenAPIParser uses config version over auto-detection.""" + from datamodel_code_generator.parser.openapi import OpenAPIParser + + parser = OpenAPIParser("", openapi_version=OpenAPIVersion.V30) + parser.raw_obj = {"openapi": "3.1.0"} + features = parser.schema_features + assert features.nullable_keyword == snapshot(True) + assert features.null_in_type_array == snapshot(False) + + +@pytest.mark.cli_doc( + options=["--schema-version"], + option_description="""Schema version to use for parsing. + +The `--schema-version` option specifies the schema version to use instead of auto-detection. +Valid values depend on input type: JsonSchema (draft-04, draft-06, draft-07, 2019-09, 2020-12) +or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field).""", + input_schema="jsonschema/simple_string.json", + cli_args=["--schema-version", "draft-07"], + golden_output="jsonschema/simple_string.py", +) +def test_cli_schema_version_jsonschema() -> None: + """Test --schema-version option with JSON Schema input.""" + from datamodel_code_generator import generate + + result = generate( + JSON_SCHEMA_DATA_PATH / "simple_string.json", + input_file_type=datamodel_code_generator.InputFileType.JsonSchema, + schema_version="draft-07", + ) + assert result is not None + assert "class Model" in result or "Model" in result + + +@pytest.mark.cli_doc( + options=["--schema-version-mode"], + option_description="""Schema version validation mode. + +The `--schema-version-mode` option controls how schema version validation is performed. +'lenient' (default): accept all features regardless of version. +'strict': warn on features outside the declared/detected version.""", + input_schema="jsonschema/simple_string.json", + cli_args=["--schema-version-mode", "lenient"], + golden_output="jsonschema/simple_string.py", +) +def test_cli_schema_version_mode() -> None: + """Test --schema-version-mode option.""" + from datamodel_code_generator import generate + + result = generate( + JSON_SCHEMA_DATA_PATH / "simple_string.json", + input_file_type=datamodel_code_generator.InputFileType.JsonSchema, + schema_version_mode=VersionMode.Lenient, + ) + assert result is not None + + +def test_schema_paths_lenient_mode_draft7() -> None: + """Test schema_paths returns both paths in Lenient mode for Draft 7.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser("", jsonschema_version=JsonSchemaVersion.Draft7) + paths = parser.schema_paths + assert paths == snapshot([ + ("#/definitions", ["definitions"]), + ("#/$defs", ["$defs"]), + ]) + + +def test_schema_paths_lenient_mode_2020_12() -> None: + """Test schema_paths returns $defs first in Lenient mode for 2020-12.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser("", jsonschema_version=JsonSchemaVersion.Draft202012) + paths = parser.schema_paths + assert paths == snapshot([ + ("#/$defs", ["$defs"]), + ("#/definitions", ["definitions"]), + ]) + + +def test_schema_paths_strict_mode_draft7() -> None: + """Test schema_paths returns only definitions in Strict mode for Draft 7.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft7, + schema_version_mode=VersionMode.Strict, + ) + paths = parser.schema_paths + assert paths == snapshot([("#/definitions", ["definitions"])]) + + +def test_schema_paths_strict_mode_2020_12() -> None: + """Test schema_paths returns only $defs in Strict mode for 2020-12.""" + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft202012, + schema_version_mode=VersionMode.Strict, + ) + paths = parser.schema_paths + assert paths == snapshot([("#/$defs", ["$defs"])]) + + +def test_openapi_schema_paths_unchanged() -> None: + """Test that OpenAPI schema_paths uses SCHEMA_PATHS regardless of version mode.""" + from datamodel_code_generator.parser.openapi import OpenAPIParser + + parser = OpenAPIParser( + "", + openapi_version=OpenAPIVersion.V31, + schema_version_mode=VersionMode.Strict, + ) + paths = parser.schema_paths + assert paths == snapshot([("#/components/schemas", ["components", "schemas"])]) + + +def test_nullable_keyword_openapi_31_strict_warning() -> None: + """Test that nullable keyword emits warning in OpenAPI 3.1 Strict mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaObject + from datamodel_code_generator.parser.openapi import OpenAPIParser + + parser = OpenAPIParser( + "", + openapi_version=OpenAPIVersion.V31, + schema_version_mode=VersionMode.Strict, + strict_nullable=True, + ) + obj = JsonSchemaObject(type="string", nullable=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser.get_data_type(obj) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "nullable keyword is deprecated" in str(w[0].message) + + +def test_nullable_keyword_openapi_30_no_warning() -> None: + """Test that nullable keyword does NOT emit warning in OpenAPI 3.0.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaObject + from datamodel_code_generator.parser.openapi import OpenAPIParser + + parser = OpenAPIParser( + "", + openapi_version=OpenAPIVersion.V30, + schema_version_mode=VersionMode.Strict, + strict_nullable=True, + ) + obj = JsonSchemaObject(type="string", nullable=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser.get_data_type(obj) + deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + assert len(deprecation_warnings) == 0 + + +def test_nullable_keyword_openapi_31_lenient_no_warning() -> None: + """Test that nullable keyword does NOT emit warning in OpenAPI 3.1 Lenient mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaObject + from datamodel_code_generator.parser.openapi import OpenAPIParser + + parser = OpenAPIParser( + "", + openapi_version=OpenAPIVersion.V31, + schema_version_mode=VersionMode.Lenient, + strict_nullable=True, + ) + obj = JsonSchemaObject(type="string", nullable=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser.get_data_type(obj) + deprecation_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + assert len(deprecation_warnings) == 0 + + +def test_null_in_type_array_strict_warning_draft7() -> None: + """Test that null in type array emits warning in Draft 7 Strict mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft7, + schema_version_mode=VersionMode.Strict, + ) + raw_schema = {"type": ["string", "null"]} + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_version_specific_features(raw_schema, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 1 + assert "null in type array" in str(user_warnings[0].message) + + +def test_null_in_type_array_no_warning_2020_12() -> None: + """Test that null in type array does NOT emit warning in Draft 2020-12.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft202012, + schema_version_mode=VersionMode.Strict, + ) + raw_schema = {"type": ["string", "null"]} + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_version_specific_features(raw_schema, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 0 + + +def test_exclusive_as_number_strict_warning_draft4() -> None: + """Test that numeric exclusiveMinimum emits warning in Draft 4 Strict mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft4, + schema_version_mode=VersionMode.Strict, + ) + raw_schema = {"type": "number", "exclusiveMinimum": 5} + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_version_specific_features(raw_schema, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 1 + assert "exclusiveMinimum as number" in str(user_warnings[0].message) + + +def test_exclusive_as_bool_strict_warning_draft7() -> None: + """Test that boolean exclusiveMinimum emits warning in Draft 7 Strict mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft7, + schema_version_mode=VersionMode.Strict, + ) + raw_schema = {"type": "number", "minimum": 5, "exclusiveMinimum": True} + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_version_specific_features(raw_schema, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 1 + assert "exclusiveMinimum as boolean" in str(user_warnings[0].message) + + +def test_prefix_items_strict_warning_draft7() -> None: + """Test that prefixItems emits warning in Draft 7 Strict mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaObject, JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft7, + schema_version_mode=VersionMode.Strict, + ) + obj = JsonSchemaObject( + type="array", + prefixItems=[JsonSchemaObject(type="string"), JsonSchemaObject(type="number")], + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_array_version_features(obj, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 1 + assert "prefixItems is not supported" in str(user_warnings[0].message) + + +def test_items_array_strict_warning_2020_12() -> None: + """Test that items as array emits warning in Draft 2020-12 Strict mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaObject, JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft202012, + schema_version_mode=VersionMode.Strict, + ) + obj = JsonSchemaObject( + type="array", + items=[JsonSchemaObject(type="string"), JsonSchemaObject(type="number")], + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_array_version_features(obj, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 1 + assert "items as array" in str(user_warnings[0].message) + + +def test_boolean_schema_strict_warning_draft4() -> None: + """Test that boolean schema emits warning in Draft 4 Strict mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft4, + schema_version_mode=VersionMode.Strict, + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_version_specific_features(True, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 1 + assert "Boolean schemas" in str(user_warnings[0].message) + + +def test_boolean_schema_no_warning_draft7() -> None: + """Test that boolean schema does NOT emit warning in Draft 7.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft7, + schema_version_mode=VersionMode.Strict, + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_version_specific_features(True, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 0 + + +def test_version_checks_lenient_no_warnings() -> None: + """Test that version checks do NOT emit warnings in Lenient mode.""" + import warnings + + from datamodel_code_generator.parser.jsonschema import JsonSchemaParser + + parser = JsonSchemaParser( + "", + jsonschema_version=JsonSchemaVersion.Draft4, + schema_version_mode=VersionMode.Lenient, + ) + raw_schema = {"type": ["string", "null"], "exclusiveMinimum": 5} + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parser._check_version_specific_features(raw_schema, ["test"]) + parser._check_version_specific_features(True, ["test"]) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert len(user_warnings) == 0 From e31aa17e95390720c77afd48462f2cdec8eab335 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 16:09:15 +0000 Subject: [PATCH 03/20] docs: update llms.txt files Generated by GitHub Actions --- docs/llms-full.txt | 80 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index bdbf5e8e0..0eadf259c 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -245,7 +245,7 @@ This documentation is auto-generated from test cases. | Category | Options | Description | |----------|---------|-------------| -| 📁 [Base Options](base-options.md) | 7 | Input/output configuration | +| 📁 [Base Options](base-options.md) | 9 | Input/output configuration | | 🔧 [Typing Customization](typing-customization.md) | 27 | Type annotation and import behavior | | 🏷️ [Field Customization](field-customization.md) | 24 | Field naming and docstring behavior | | 🏗️ [Model Customization](model-customization.md) | 39 | Model generation behavior | @@ -398,6 +398,8 @@ This documentation is auto-generated from test cases. ### S {#s} +- [`--schema-version`](base-options.md#schema-version) +- [`--schema-version-mode`](base-options.md#schema-version-mode) - [`--set-default-enum-member`](field-customization.md#set-default-enum-member) - [`--shared-module-name`](general-options.md#shared-module-name) - [`--skip-root-model`](model-customization.md#skip-root-model) @@ -481,6 +483,8 @@ Source: https://datamodel-code-generator.koxudaxi.dev/cli-reference/base-options | [`--input-model`](#input-model) | Import a Python type or dict schema from a module. | | [`--input-model-ref-strategy`](#input-model-ref-strategy) | Strategy for referenced types when using --input-model. | | [`--output`](#output) | Specify the destination path for generated Python code. | +| [`--schema-version`](#schema-version) | Schema version to use for parsing. | +| [`--schema-version-mode`](#schema-version-mode) | Schema version validation mode. | | [`--url`](#url) | Fetch schema from URL with custom HTTP headers. | --- @@ -797,6 +801,76 @@ is written to stdout. --- +## `--schema-version` {#schema-version} + +Schema version to use for parsing. + +The `--schema-version` option specifies the schema version to use instead of auto-detection. +Valid values depend on input type: JsonSchema (draft-04, draft-06, draft-07, 2019-09, 2020-12) +or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field). + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --schema-version draft-07 # (1)! + ``` + + 1. :material-arrow-left: `--schema-version` - the option documented here + +??? example "Examples" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": {"s": {"type": ["string"]}}, + "required": ["s"] + } + ``` + + **Output:** + + > **Error:** File not found: jsonschema/simple_string.py + +--- + +## `--schema-version-mode` {#schema-version-mode} + +Schema version validation mode. + +The `--schema-version-mode` option controls how schema version validation is performed. +'lenient' (default): accept all features regardless of version. +'strict': warn on features outside the declared/detected version. + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --schema-version-mode lenient # (1)! + ``` + + 1. :material-arrow-left: `--schema-version-mode` - the option documented here + +??? example "Examples" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": {"s": {"type": ["string"]}}, + "required": ["s"] + } + ``` + + **Output:** + + > **Error:** File not found: jsonschema/simple_string.py + +--- + ## `--url` {#url} Fetch schema from URL with custom HTTP headers. @@ -23161,6 +23235,8 @@ datamodel-codegen [OPTIONS] | [`--input-model`](base-options.md#input-model) | Import a Python type or dict schema from a module. | | [`--input-model-ref-strategy`](base-options.md#input-model-ref-strategy) | Strategy for referenced types when using --input-model. | | [`--output`](base-options.md#output) | Specify the destination path for generated Python code. | +| [`--schema-version`](base-options.md#schema-version) | Schema version to use for parsing. | +| [`--schema-version-mode`](base-options.md#schema-version-mode) | Schema version validation mode. | | [`--url`](base-options.md#url) | Fetch schema from URL with custom HTTP headers. | ### 🔧 Typing Customization @@ -23438,6 +23514,8 @@ All options sorted alphabetically: - [`--remove-special-field-name-prefix`](field-customization.md#remove-special-field-name-prefix) - Remove the special prefix from field names. - [`--reuse-model`](model-customization.md#reuse-model) - Reuse identical model definitions instead of generating dupl... - [`--reuse-scope`](model-customization.md#reuse-scope) - Scope for model reuse detection (root or tree). +- [`--schema-version`](base-options.md#schema-version) - Schema version to use for parsing. +- [`--schema-version-mode`](base-options.md#schema-version-mode) - Schema version validation mode. - [`--set-default-enum-member`](field-customization.md#set-default-enum-member) - Set the first enum member as the default value for enum fiel... - [`--shared-module-name`](general-options.md#shared-module-name) - Customize the name of the shared module for deduplicated mod... - [`--skip-root-model`](model-customization.md#skip-root-model) - Skip generation of root model when schema contains nested de... From d382f2656b74018bcda32244b89862f695108446 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 16:09:38 +0000 Subject: [PATCH 04/20] docs: update CLI reference documentation and prompt data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- src/datamodel_code_generator/prompt_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/datamodel_code_generator/prompt_data.py b/src/datamodel_code_generator/prompt_data.py index 00100dc4a..40e48615f 100644 --- a/src/datamodel_code_generator/prompt_data.py +++ b/src/datamodel_code_generator/prompt_data.py @@ -93,6 +93,8 @@ "--remove-special-field-name-prefix": "Remove the special prefix from field names.", "--reuse-model": "Reuse identical model definitions instead of generating duplicates.", "--reuse-scope": "Scope for model reuse detection (root or tree).", + "--schema-version": "Schema version to use for parsing.", + "--schema-version-mode": "Schema version validation mode.", "--set-default-enum-member": "Set the first enum member as the default value for enum fields.", "--shared-module-name": "Customize the name of the shared module for deduplicated models.", "--skip-root-model": "Skip generation of root model when schema contains nested definitions.", From eef2db705168eec07d8f54081c6b1591350fa043 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 16:38:58 +0000 Subject: [PATCH 05/20] Add SchemaFeaturesT generic type parameter to Parser --- docs/supported_formats.md | 148 ++++++++++++++++++ src/datamodel_code_generator/parser/base.py | 18 ++- .../parser/graphql.py | 13 +- .../parser/jsonschema.py | 2 +- tests/parser/test_base.py | 14 +- tests/parser/test_graphql.py | 25 +++ 6 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 docs/supported_formats.md diff --git a/docs/supported_formats.md b/docs/supported_formats.md new file mode 100644 index 000000000..ca13172e9 --- /dev/null +++ b/docs/supported_formats.md @@ -0,0 +1,148 @@ +# Schema Version Support + +This document describes the JSON Schema and OpenAPI versions supported by datamodel-code-generator. + +## Overview + +datamodel-code-generator supports multiple versions of JSON Schema and OpenAPI specifications. By default, the tool operates in **Lenient mode**, accepting all features regardless of version declarations. This ensures maximum compatibility with real-world schemas that often mix features from different versions. + +## JSON Schema Version Support + +### Supported Versions + +| Version | Spec URL | Notes | +|---------|----------|-------| +| Draft 4 | [json-schema.org/draft-04](https://json-schema.org/draft-04/json-schema-core) | `id`, `definitions` | +| Draft 6 | [json-schema.org/draft-06](https://json-schema.org/draft-06/json-schema-release-notes) | `$id`, const, boolean schemas | +| Draft 7 | [json-schema.org/draft-07](https://json-schema.org/draft-07/json-schema-release-notes) | if/then/else, readOnly/writeOnly | +| 2019-09 | [json-schema.org/draft/2019-09](https://json-schema.org/draft/2019-09/release-notes) | `$defs`, `$anchor` | +| 2020-12 | [json-schema.org/draft/2020-12](https://json-schema.org/draft/2020-12/release-notes) | `prefixItems`, null in type arrays | + +### Feature Compatibility Matrix + +| Feature | Draft 4 | Draft 6 | Draft 7 | 2019-09 | 2020-12 | +|---------|---------|---------|---------|---------|---------| +| **ID/Reference** | +| ID field | `id` | `$id` | `$id` | `$id` | `$id` | +| Definitions key | `definitions` | `definitions` | `definitions` | `$defs`* | `$defs` | +| **Type Features** | +| Boolean schemas | - | Yes | Yes | Yes | Yes | +| Null in type array | - | - | - | - | Yes | +| const | - | Yes | Yes | Yes | Yes | +| **Numeric Constraints** | +| exclusiveMinimum (number) | - (boolean) | Yes | Yes | Yes | Yes | +| exclusiveMaximum (number) | - (boolean) | Yes | Yes | Yes | Yes | +| **Array Features** | +| prefixItems | - | - | - | - | Yes | +| items (as array/tuple) | Yes | Yes | Yes | Yes | - (single only) | +| contains | - | Yes | Yes | Yes | Yes | +| **Conditional** | +| if/then/else | - | - | Yes | Yes | Yes | +| **Metadata** | +| readOnly | - | - | Yes | Yes | Yes | +| writeOnly | - | - | Yes | Yes | Yes | + +*2019-09 supports both `definitions` and `$defs` for backward compatibility. + +### Version Detection + +datamodel-code-generator automatically detects the JSON Schema version: + +1. **Explicit `$schema` field**: If present, the version is detected from the URL pattern +2. **Heuristics**: If no `$schema`, presence of `$defs` suggests 2020-12, `definitions` suggests Draft 7 +3. **Fallback**: Draft 7 (most widely used) + +## OpenAPI Version Support + +### Supported Versions + +| Version | Spec URL | JSON Schema Base | +|---------|----------|------------------| +| 3.0.x | [spec.openapis.org/oas/v3.0.3](https://spec.openapis.org/oas/v3.0.3) | Draft 5 (subset) | +| 3.1.x | [spec.openapis.org/oas/v3.1.0](https://spec.openapis.org/oas/v3.1.0) | 2020-12 (full) | + +> **Note**: OpenAPI 2.0 (Swagger) support is limited. We recommend converting to OpenAPI 3.0+. + +### Feature Compatibility Matrix + +| Feature | OAS 3.0 | OAS 3.1 | +|---------|---------|---------| +| **Schema Base** | JSON Schema Draft 5 (subset) | JSON Schema 2020-12 (full) | +| **Definitions Path** | `#/components/schemas` | `#/components/schemas` | +| **Nullable** | +| `nullable: true` keyword | Yes | Deprecated | +| Null in type array | - | Yes | +| Type as array | - | Yes | +| **Array Features** | +| prefixItems | - | Yes | +| **Boolean Schemas** | - | Yes | +| **OpenAPI Specific** | +| discriminator | Yes | Yes | +| binary format | Yes | Yes | +| password format | Yes | Yes | +| webhooks | - | Yes | + +### Version Detection + +datamodel-code-generator detects the OpenAPI version from the `openapi` field: + +- `openapi: "3.0.x"` -> OpenAPI 3.0 +- `openapi: "3.1.x"` -> OpenAPI 3.1 +- No `openapi` field -> Fallback to OpenAPI 3.1 + +## Data Format Support + +### Common Formats (JSON Schema + OpenAPI) + +| Type | Format | Python Type | +|------|--------|-------------| +| integer | int32 | `int` | +| integer | int64 | `int` | +| number | float | `float` | +| number | double | `float` | +| number | decimal | `Decimal` | +| string | date | `date` | +| string | date-time | `datetime` | +| string | time | `time` | +| string | duration | `timedelta` | +| string | email | `EmailStr` | +| string | uri | `AnyUrl` | +| string | uuid | `UUID` | +| string | byte | `bytes` (base64) | +| string | ipv4 | `IPv4Address` | +| string | ipv6 | `IPv6Address` | +| string | hostname | `str` | + +### OpenAPI-Only Formats + +| Type | Format | Python Type | Notes | +|------|--------|-------------|-------| +| string | binary | `bytes` | File content | +| string | password | `SecretStr` | Sensitive data | + +## Limitations and Known Issues + +### JSON Schema + +1. **`$anchor` and `$dynamicRef`**: Not fully implemented +2. **`unevaluatedProperties`/`unevaluatedItems`**: Not implemented +3. **`contentMediaType`/`contentEncoding`**: Not implemented + +### OpenAPI + +1. **OpenAPI 2.0 (Swagger)**: Limited support, recommend converting to 3.0+ +2. **`$ref` sibling keywords in 3.0**: Not supported (3.0 spec limitation) + +### Mixed Version Schemas + +Real-world schemas often mix features from different versions. datamodel-code-generator handles this in **Lenient mode** (default): + +- Features from all versions are accepted +- No warnings for version mismatches +- Maximum compatibility with existing schemas + +## See Also + +- [Supported Data Types](./supported-data-types.md) - Complete data type support +- [JSON Schema Guide](./jsonschema.md) - JSON Schema usage examples +- [OpenAPI Guide](./openapi.md) - OpenAPI usage examples diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 37b02a5b5..6067c4d94 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -89,8 +89,10 @@ from datamodel_code_generator._types import ParserConfigDict from datamodel_code_generator.config import ParserConfig + from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures ParserConfigT = TypeVar("ParserConfigT", bound="ParserConfig") +SchemaFeaturesT = TypeVar("SchemaFeaturesT", bound="JsonSchemaFeatures") @runtime_checkable @@ -688,13 +690,27 @@ def from_dict(cls, data: dict[str, YamlValue]) -> Source: return cls(path=Path(), raw_data=data) -class Parser(ABC, Generic[ParserConfigT]): +class Parser(ABC, Generic[ParserConfigT, SchemaFeaturesT]): """Abstract base class for schema parsers. Provides the parsing algorithm and code generation. Subclasses implement parse_raw() to handle specific schema formats. + + Type Parameters: + ParserConfigT: The configuration type for this parser. + SchemaFeaturesT: The schema features type (JsonSchemaFeatures or subclass). """ + @property + @abstractmethod + def schema_features(self) -> SchemaFeaturesT: + """Get schema features based on detected version. + + Returns: + Schema features instance with version-specific flags. + """ + ... + @classmethod def _create_default_config(cls, options: ParserConfigDict) -> ParserConfigT: # ty: ignore """Create a default config from options. diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index c7f2e673f..17ef94f8d 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -6,6 +6,7 @@ from __future__ import annotations +from functools import cached_property from typing import ( TYPE_CHECKING, Any, @@ -43,6 +44,7 @@ from datamodel_code_generator._types import GraphQLParserConfigDict from datamodel_code_generator.config import GraphQLParserConfig from datamodel_code_generator.model import DataModel, DataModelFieldBase + from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures # graphql-core >=3.2.7 removed TypeResolvers in favor of TypeFields.kind. # Normalize to a single callable for resolving type kinds. @@ -59,11 +61,20 @@ def build_graphql_schema(schema_str: str) -> graphql.GraphQLSchema: @snooper_to_methods() -class GraphQLParser(Parser["GraphQLParserConfig"]): +class GraphQLParser(Parser["GraphQLParserConfig", "JsonSchemaFeatures"]): """Parser for GraphQL schema files.""" # raw graphql schema as `graphql-core` object raw_obj: graphql.GraphQLSchema + + @cached_property + def schema_features(self) -> JsonSchemaFeatures: + """Get schema features for GraphQL (uses default JSON Schema features).""" + from datamodel_code_generator.enums import JsonSchemaVersion # noqa: PLC0415 + from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures # noqa: PLC0415 + + return JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft202012) + # all processed graphql objects # mapper from an object name (unique) to an object all_graphql_objects: dict[str, graphql.GraphQLNamedType] diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 32312cf67..40ecea343 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -564,7 +564,7 @@ def _get_type( @snooper_to_methods() # noqa: PLR0904 -class JsonSchemaParser(Parser["JSONSchemaParserConfig"]): +class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]): """Parser for JSON Schema, JSON, YAML, Dict, and CSV formats.""" SCHEMA_PATHS: ClassVar[list[str]] = ["#/definitions", "#/$defs"] diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 53dc9b071..37ea386bf 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -3,12 +3,16 @@ from __future__ import annotations from collections import OrderedDict -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock import pytest from datamodel_code_generator.model import DataModel, DataModelFieldBase + +if TYPE_CHECKING: + from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures + from datamodel_code_generator.model.pydantic import BaseModel, DataModelField from datamodel_code_generator.model.type_alias import TypeAlias, TypeAliasTypeBackport, TypeStatement from datamodel_code_generator.parser.base import ( @@ -36,6 +40,14 @@ class B(DataModel): class C(Parser): """Test parser class C.""" + @property + def schema_features(self) -> JsonSchemaFeatures: + """Return mock schema features.""" + from datamodel_code_generator.enums import JsonSchemaVersion + from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures + + return JsonSchemaFeatures.from_version(JsonSchemaVersion.Draft202012) + def parse_raw(self, name: str, raw: dict[str, Any]) -> None: """Parse raw data into models.""" diff --git a/tests/parser/test_graphql.py b/tests/parser/test_graphql.py index af8825133..26a528643 100644 --- a/tests/parser/test_graphql.py +++ b/tests/parser/test_graphql.py @@ -145,3 +145,28 @@ def assert_typename_present(output_path: Path, _: str | None, **_kwargs: object) assert_func=assert_typename_present, expected_file=None, ) + + +def test_graphql_schema_features() -> None: + """Test that GraphQLParser has schema_features property returning JsonSchemaFeatures.""" + from inline_snapshot import snapshot + + from datamodel_code_generator.parser.schema_version import JsonSchemaFeatures + + parser = GraphQLParser( + source="type Query { id: ID }", + data_model_type=DataClass, + ) + + features = parser.schema_features + assert isinstance(features, JsonSchemaFeatures) + assert features == snapshot( + JsonSchemaFeatures( + null_in_type_array=True, + defs_not_definitions=True, + prefix_items=True, + boolean_schemas=True, + id_field="$id", + definitions_key="$defs", + ) + ) From 21f338f3302dc25667ec3b4f5f99597d0ed6f136 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 16:44:13 +0000 Subject: [PATCH 06/20] Fix test snapshot: add exclusive_as_number field --- tests/parser/test_graphql.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/parser/test_graphql.py b/tests/parser/test_graphql.py index 26a528643..3f7732617 100644 --- a/tests/parser/test_graphql.py +++ b/tests/parser/test_graphql.py @@ -168,5 +168,6 @@ def test_graphql_schema_features() -> None: boolean_schemas=True, id_field="$id", definitions_key="$defs", + exclusive_as_number=True, ) ) From de56f28dd1f32d1215996473b73586463787f463 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 16:55:15 +0000 Subject: [PATCH 07/20] Refactor: genericize _create_default_config using _get_config_class --- src/datamodel_code_generator/parser/base.py | 25 +++++++++++----- .../parser/graphql.py | 29 +++---------------- .../parser/jsonschema.py | 26 ++--------------- .../parser/openapi.py | 29 +++---------------- 4 files changed, 29 insertions(+), 80 deletions(-) diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 6067c4d94..8d3d9ff62 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -711,19 +711,30 @@ def schema_features(self) -> SchemaFeaturesT: """ ... + @classmethod + def _get_config_class(cls) -> type[ParserConfig]: + """Return the config class for this parser. + + Subclasses should override this to return their specific config class. + """ + from datamodel_code_generator.config import ParserConfig # noqa: PLC0415 + + return ParserConfig + @classmethod def _create_default_config(cls, options: ParserConfigDict) -> ParserConfigT: # ty: ignore """Create a default config from options. - Subclasses should override this to return their own config type. + Uses _get_config_class() to determine which config class to instantiate. """ from datamodel_code_generator import types as types_module # noqa: PLC0415 - from datamodel_code_generator.config import ParserConfig # noqa: PLC0415 from datamodel_code_generator.model import base as model_base # noqa: PLC0415 from datamodel_code_generator.util import is_pydantic_v2 # noqa: PLC0415 + config_class = cls._get_config_class() + if is_pydantic_v2(): - ParserConfig.model_rebuild( + config_class.model_rebuild( _types_namespace={ "StrictTypes": types_module.StrictTypes, "DataModel": model_base.DataModel, @@ -731,16 +742,16 @@ def _create_default_config(cls, options: ParserConfigDict) -> ParserConfigT: # "DataTypeManager": types_module.DataTypeManager, } ) - return ParserConfig.model_validate(options) # type: ignore[return-value] - ParserConfig.update_forward_refs( + return config_class.model_validate(options) # type: ignore[return-value] + config_class.update_forward_refs( StrictTypes=types_module.StrictTypes, DataModel=model_base.DataModel, DataModelFieldBase=model_base.DataModelFieldBase, DataTypeManager=types_module.DataTypeManager, ) - defaults = {name: field.default for name, field in ParserConfig.__fields__.items()} + defaults = {name: field.default for name, field in config_class.__fields__.items()} defaults.update(options) # ty: ignore - return ParserConfig.construct(**defaults) # type: ignore[return-value] + return config_class.construct(**defaults) # type: ignore[return-value] def __init__( # noqa: PLR0912, PLR0915 self, diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index 17ef94f8d..3089d9947 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -98,32 +98,11 @@ def schema_features(self) -> JsonSchemaFeatures: ] @classmethod - def _create_default_config(cls, options: GraphQLParserConfigDict) -> GraphQLParserConfig: # type: ignore[override] - """Create a GraphQLParserConfig from options.""" - from datamodel_code_generator import types as types_module # noqa: PLC0415 + def _get_config_class(cls) -> type[GraphQLParserConfig]: + """Return the GraphQLParserConfig class.""" from datamodel_code_generator.config import GraphQLParserConfig # noqa: PLC0415 - from datamodel_code_generator.model import base as model_base # noqa: PLC0415 - from datamodel_code_generator.util import is_pydantic_v2 # noqa: PLC0415 - - if is_pydantic_v2(): - GraphQLParserConfig.model_rebuild( - _types_namespace={ - "StrictTypes": types_module.StrictTypes, - "DataModel": model_base.DataModel, - "DataModelFieldBase": model_base.DataModelFieldBase, - "DataTypeManager": types_module.DataTypeManager, - } - ) - return GraphQLParserConfig.model_validate(options) - GraphQLParserConfig.update_forward_refs( # pragma: no cover - StrictTypes=types_module.StrictTypes, - DataModel=model_base.DataModel, - DataModelFieldBase=model_base.DataModelFieldBase, - DataTypeManager=types_module.DataTypeManager, - ) - defaults = {name: field.default for name, field in GraphQLParserConfig.__fields__.items()} # pragma: no cover - defaults.update(options) # pragma: no cover # ty: ignore - return GraphQLParserConfig.construct(**defaults) # type: ignore[return-value] # pragma: no cover + + return GraphQLParserConfig def __init__( self, diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 40ecea343..403239940 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -670,31 +670,11 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]): }) @classmethod - def _create_default_config(cls, options: JSONSchemaParserConfigDict) -> JSONSchemaParserConfig: # type: ignore[override] - """Create a JSONSchemaParserConfig from options.""" - from datamodel_code_generator import types as types_module # noqa: PLC0415 + def _get_config_class(cls) -> type[JSONSchemaParserConfig]: + """Return the JSONSchemaParserConfig class.""" from datamodel_code_generator.config import JSONSchemaParserConfig # noqa: PLC0415 - from datamodel_code_generator.model import base as model_base # noqa: PLC0415 - if is_pydantic_v2(): - JSONSchemaParserConfig.model_rebuild( - _types_namespace={ - "StrictTypes": types_module.StrictTypes, - "DataModel": model_base.DataModel, - "DataModelFieldBase": model_base.DataModelFieldBase, - "DataTypeManager": types_module.DataTypeManager, - } - ) - return JSONSchemaParserConfig.model_validate(options) - JSONSchemaParserConfig.update_forward_refs( - StrictTypes=types_module.StrictTypes, - DataModel=model_base.DataModel, - DataModelFieldBase=model_base.DataModelFieldBase, - DataTypeManager=types_module.DataTypeManager, - ) - defaults = {name: field.default for name, field in JSONSchemaParserConfig.__fields__.items()} # ty: ignore - defaults.update(options) # ty: ignore - return JSONSchemaParserConfig.construct(**defaults) # type: ignore[return-value] # pragma: no cover + return JSONSchemaParserConfig def __init__( self, diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 4b86f8908..9016f58f9 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -185,32 +185,11 @@ def schema_features(self) -> OpenAPISchemaFeatures: return OpenAPISchemaFeatures.from_openapi_version(version) @classmethod - def _create_default_config(cls, options: OpenAPIParserConfigDict) -> OpenAPIParserConfig: # ty: ignore - """Create an OpenAPIParserConfig from options.""" - from datamodel_code_generator import types as types_module # noqa: PLC0415 + def _get_config_class(cls) -> type[OpenAPIParserConfig]: + """Return the OpenAPIParserConfig class.""" from datamodel_code_generator.config import OpenAPIParserConfig # noqa: PLC0415 - from datamodel_code_generator.model import base as model_base # noqa: PLC0415 - from datamodel_code_generator.util import is_pydantic_v2 # noqa: PLC0415 - - if is_pydantic_v2(): - OpenAPIParserConfig.model_rebuild( - _types_namespace={ - "StrictTypes": types_module.StrictTypes, - "DataModel": model_base.DataModel, - "DataModelFieldBase": model_base.DataModelFieldBase, - "DataTypeManager": types_module.DataTypeManager, - } - ) - return OpenAPIParserConfig.model_validate(options) - OpenAPIParserConfig.update_forward_refs( - StrictTypes=types_module.StrictTypes, - DataModel=model_base.DataModel, - DataModelFieldBase=model_base.DataModelFieldBase, - DataTypeManager=types_module.DataTypeManager, - ) - defaults = {name: field.default for name, field in OpenAPIParserConfig.__fields__.items()} - defaults.update(options) # ty: ignore - return OpenAPIParserConfig.construct(**defaults) # type: ignore[return-value] # pragma: no cover + + return OpenAPIParserConfig def __init__( self, From 2cbbda682c0d9403e045001ee08d8a1ab1764f1a Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 17:03:29 +0000 Subject: [PATCH 08/20] Add parameterized e2e tests for --schema-version and --schema-version-mode --- tests/parser/test_schema_version.py | 123 ++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py index 2fddf861c..0cc0f4afb 100644 --- a/tests/parser/test_schema_version.py +++ b/tests/parser/test_schema_version.py @@ -829,3 +829,126 @@ def test_version_checks_lenient_no_warnings() -> None: parser._check_version_specific_features(True, ["test"]) user_warnings = [x for x in w if issubclass(x.category, UserWarning)] assert len(user_warnings) == 0 + + +# ============================================================================= +# Parameterized E2E tests for --schema-version and --schema-version-mode +# ============================================================================= + +OPENAPI_DATA_PATH = Path(__file__).parent.parent / "data" / "openapi" + + +@pytest.mark.parametrize( + "schema_version", + ["draft-04", "draft-06", "draft-07", "2019-09", "2020-12"], + ids=["draft-04", "draft-06", "draft-07", "2019-09", "2020-12"], +) +@pytest.mark.cli_doc( + options=["--schema-version"], + option_description="""Schema version to use for parsing JSON Schema. + +The `--schema-version` option specifies the JSON Schema version to use instead of auto-detection. +Valid values: draft-04, draft-06, draft-07, 2019-09, 2020-12. +Default is 'auto' (detected from $schema field).""", + input_schema="jsonschema/simple_string.json", + cli_args=["--schema-version", "draft-07"], + golden_output="jsonschema/simple_string.py", +) +def test_cli_schema_version_jsonschema_parametrized(schema_version: str) -> None: + """Test --schema-version option with different JSON Schema versions.""" + from datamodel_code_generator import generate + + result = generate( + JSON_SCHEMA_DATA_PATH / "simple_string.json", + input_file_type=datamodel_code_generator.InputFileType.JsonSchema, + schema_version=schema_version, + disable_timestamp=True, + ) + assert result is not None + assert "class Model" in result + assert result == snapshot( + """\ +# generated by datamodel-codegen: +# filename: simple_string.json + +from __future__ import annotations + +from pydantic import BaseModel + + +class Model(BaseModel): + s: str""" + ) + + +@pytest.mark.parametrize( + "openapi_version", + ["3.0", "3.1"], + ids=["openapi-3.0", "openapi-3.1"], +) +@pytest.mark.cli_doc( + options=["--schema-version"], + option_description="""Schema version to use for parsing OpenAPI. + +The `--schema-version` option specifies the OpenAPI version to use instead of auto-detection. +Valid values: 3.0, 3.1. +Default is 'auto' (detected from openapi field).""", + input_schema="openapi/api.yaml", + cli_args=["--schema-version", "3.0"], + golden_output="openapi/api.py", +) +def test_cli_schema_version_openapi_parametrized(openapi_version: str) -> None: + """Test --schema-version option with different OpenAPI versions.""" + from datamodel_code_generator import generate + + result = generate( + OPENAPI_DATA_PATH / "api.yaml", + input_file_type=datamodel_code_generator.InputFileType.OpenAPI, + schema_version=openapi_version, + disable_timestamp=True, + ) + assert result is not None + assert "Pet" in result or "Pets" in result + + +@pytest.mark.parametrize( + "version_mode", + [VersionMode.Lenient, VersionMode.Strict], + ids=["lenient", "strict"], +) +@pytest.mark.cli_doc( + options=["--schema-version-mode"], + option_description="""Schema version validation mode. + +The `--schema-version-mode` option controls how schema version validation is performed. +'lenient' (default): accept all features regardless of version. +'strict': warn on features outside the declared/detected version.""", + input_schema="jsonschema/simple_string.json", + cli_args=["--schema-version-mode", "lenient"], + golden_output="jsonschema/simple_string.py", +) +def test_cli_schema_version_mode_parametrized(version_mode: VersionMode) -> None: + """Test --schema-version-mode option with different modes.""" + from datamodel_code_generator import generate + + result = generate( + JSON_SCHEMA_DATA_PATH / "simple_string.json", + input_file_type=datamodel_code_generator.InputFileType.JsonSchema, + schema_version_mode=version_mode, + disable_timestamp=True, + ) + assert result is not None + assert "class Model" in result + assert result == snapshot( + """\ +# generated by datamodel-codegen: +# filename: simple_string.json + +from __future__ import annotations + +from pydantic import BaseModel + + +class Model(BaseModel): + s: str""" + ) From b0c51015a9b5d077613fbd89c60c2b57615ccddd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:03:54 +0000 Subject: [PATCH 09/20] docs: update CLI reference documentation and prompt data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- docs/cli-reference/base-options.md | 214 +++++++++++++++++++++++++++-- 1 file changed, 203 insertions(+), 11 deletions(-) diff --git a/docs/cli-reference/base-options.md b/docs/cli-reference/base-options.md index 1f633a906..1fdf26c98 100644 --- a/docs/cli-reference/base-options.md +++ b/docs/cli-reference/base-options.md @@ -346,20 +346,212 @@ or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field ??? example "Examples" - **Input Schema:** + === "OpenAPI" - ```json - { - "$schema": "http://json-schema.org/draft-07/schema", - "type": "object", - "properties": {"s": {"type": ["string"]}}, - "required": ["s"] - } - ``` + **Input Schema:** - **Output:** + ```yaml + openapi: "3.0.0" + info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT + servers: + - url: http://petstore.swagger.io/v1 + paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + default: 1 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Users: + type: array + items: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Id: + type: string + Rules: + type: array + items: + type: string + Error: + description: error result + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + apis: + type: array + items: + type: object + properties: + apiKey: + type: string + description: To be used as a dataset parameter value + apiVersionNumber: + type: string + description: To be used as a version parameter value + apiUrl: + type: string + format: uri + description: "The URL describing the dataset's fields" + apiDocumentationUrl: + type: string + format: uri + description: A URL to the API console for each API + Event: + type: object + description: Event object + properties: + name: + type: string + Result: + type: object + properties: + event: + $ref: '#/components/schemas/Event' + ``` - > **Error:** File not found: jsonschema/simple_string.py + **Output:** + + > **Error:** File not found: openapi/api.py + + === "JSON Schema" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": {"s": {"type": ["string"]}}, + "required": ["s"] + } + ``` + + **Output:** + + > **Error:** File not found: jsonschema/simple_string.py --- From c81aaa8d58be0b60d10eced2fcea899febfcc22a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:04:15 +0000 Subject: [PATCH 10/20] docs: update llms.txt files Generated by GitHub Actions --- docs/llms-full.txt | 214 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 203 insertions(+), 11 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 0eadf259c..1a2b27350 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -819,20 +819,212 @@ or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field ??? example "Examples" - **Input Schema:** + === "OpenAPI" - ```json - { - "$schema": "http://json-schema.org/draft-07/schema", - "type": "object", - "properties": {"s": {"type": ["string"]}}, - "required": ["s"] - } - ``` + **Input Schema:** - **Output:** + ```yaml + openapi: "3.0.0" + info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT + servers: + - url: http://petstore.swagger.io/v1 + paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + x-amazon-apigateway-integration: + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PythonVersionFunction.Arn}/invocations + passthroughBehavior: when_no_templates + httpMethod: POST + type: aws_proxy + components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + default: 1 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + Users: + type: array + items: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Id: + type: string + Rules: + type: array + items: + type: string + Error: + description: error result + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + apis: + type: array + items: + type: object + properties: + apiKey: + type: string + description: To be used as a dataset parameter value + apiVersionNumber: + type: string + description: To be used as a version parameter value + apiUrl: + type: string + format: uri + description: "The URL describing the dataset's fields" + apiDocumentationUrl: + type: string + format: uri + description: A URL to the API console for each API + Event: + type: object + description: Event object + properties: + name: + type: string + Result: + type: object + properties: + event: + $ref: '#/components/schemas/Event' + ``` - > **Error:** File not found: jsonschema/simple_string.py + **Output:** + + > **Error:** File not found: openapi/api.py + + === "JSON Schema" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema", + "type": "object", + "properties": {"s": {"type": ["string"]}}, + "required": ["s"] + } + ``` + + **Output:** + + > **Error:** File not found: jsonschema/simple_string.py --- From 943a8f89fea1d5b9c84731e293b27583ed7a6137 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 17:11:07 +0000 Subject: [PATCH 11/20] Refactor: use _config_class_name class variable instead of method override --- src/datamodel_code_generator/parser/base.py | 11 ++++++++--- src/datamodel_code_generator/parser/graphql.py | 8 ++------ src/datamodel_code_generator/parser/jsonschema.py | 7 +------ src/datamodel_code_generator/parser/openapi.py | 7 +------ tests/data/expected/main/jsonschema/simple_string.py | 10 ++++++++++ 5 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 tests/data/expected/main/jsonschema/simple_string.py diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 8d3d9ff62..487251bdc 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -19,6 +19,7 @@ from typing import ( TYPE_CHECKING, Any, + ClassVar, Generic, NamedTuple, Optional, @@ -711,15 +712,19 @@ def schema_features(self) -> SchemaFeaturesT: """ ... + _config_class_name: ClassVar[str] = "ParserConfig" + @classmethod def _get_config_class(cls) -> type[ParserConfig]: """Return the config class for this parser. - Subclasses should override this to return their specific config class. + Uses _config_class_name class variable to dynamically import the config class. + Subclasses should set _config_class_name to their config class name. """ - from datamodel_code_generator.config import ParserConfig # noqa: PLC0415 + import importlib # noqa: PLC0415 - return ParserConfig + module = importlib.import_module("datamodel_code_generator.config") + return getattr(module, cls._config_class_name) @classmethod def _create_default_config(cls, options: ParserConfigDict) -> ParserConfigT: # ty: ignore diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index 3089d9947..82f7e7051 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -10,6 +10,7 @@ from typing import ( TYPE_CHECKING, Any, + ClassVar, ) from typing_extensions import Unpack @@ -97,12 +98,7 @@ def schema_features(self) -> JsonSchemaFeatures: graphql.type.introspection.TypeKind.UNION, ] - @classmethod - def _get_config_class(cls) -> type[GraphQLParserConfig]: - """Return the GraphQLParserConfig class.""" - from datamodel_code_generator.config import GraphQLParserConfig # noqa: PLC0415 - - return GraphQLParserConfig + _config_class_name: ClassVar[str] = "GraphQLParserConfig" def __init__( self, diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 403239940..b15b4cfca 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -669,12 +669,7 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig", "JsonSchemaFeatures"]): "ChainMap", }) - @classmethod - def _get_config_class(cls) -> type[JSONSchemaParserConfig]: - """Return the JSONSchemaParserConfig class.""" - from datamodel_code_generator.config import JSONSchemaParserConfig # noqa: PLC0415 - - return JSONSchemaParserConfig + _config_class_name: ClassVar[str] = "JSONSchemaParserConfig" def __init__( self, diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 9016f58f9..b78fb6a82 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -184,12 +184,7 @@ def schema_features(self) -> OpenAPISchemaFeatures: version = detect_openapi_version(self.raw_obj) if self.raw_obj else OpenAPIVersion.Auto return OpenAPISchemaFeatures.from_openapi_version(version) - @classmethod - def _get_config_class(cls) -> type[OpenAPIParserConfig]: - """Return the OpenAPIParserConfig class.""" - from datamodel_code_generator.config import OpenAPIParserConfig # noqa: PLC0415 - - return OpenAPIParserConfig + _config_class_name: ClassVar[str] = "OpenAPIParserConfig" def __init__( self, diff --git a/tests/data/expected/main/jsonschema/simple_string.py b/tests/data/expected/main/jsonschema/simple_string.py new file mode 100644 index 000000000..03ef5d9cf --- /dev/null +++ b/tests/data/expected/main/jsonschema/simple_string.py @@ -0,0 +1,10 @@ +# generated by datamodel-codegen: +# filename: simple_string.json + +from __future__ import annotations + +from pydantic import BaseModel + + +class Model(BaseModel): + s: str From 17741e19f4bcc06e297669015420ffa03aac0494 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:11:41 +0000 Subject: [PATCH 12/20] docs: update CLI reference documentation and prompt data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- docs/cli-reference/base-options.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/cli-reference/base-options.md b/docs/cli-reference/base-options.md index 1fdf26c98..bd0721ffb 100644 --- a/docs/cli-reference/base-options.md +++ b/docs/cli-reference/base-options.md @@ -551,7 +551,18 @@ or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field **Output:** - > **Error:** File not found: jsonschema/simple_string.py + ```python + # generated by datamodel-codegen: + # filename: simple_string.json + + from __future__ import annotations + + from pydantic import BaseModel + + + class Model(BaseModel): + s: str + ``` --- @@ -586,7 +597,18 @@ The `--schema-version-mode` option controls how schema version validation is per **Output:** - > **Error:** File not found: jsonschema/simple_string.py + ```python + # generated by datamodel-codegen: + # filename: simple_string.json + + from __future__ import annotations + + from pydantic import BaseModel + + + class Model(BaseModel): + s: str + ``` --- From c83f9c194507db9b85b90bf94a3e339b3a4d5383 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:12:03 +0000 Subject: [PATCH 13/20] docs: update llms.txt files Generated by GitHub Actions --- docs/llms-full.txt | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 1a2b27350..ac6577d0a 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -1024,7 +1024,18 @@ or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field **Output:** - > **Error:** File not found: jsonschema/simple_string.py + ```python + # generated by datamodel-codegen: + # filename: simple_string.json + + from __future__ import annotations + + from pydantic import BaseModel + + + class Model(BaseModel): + s: str + ``` --- @@ -1059,7 +1070,18 @@ The `--schema-version-mode` option controls how schema version validation is per **Output:** - > **Error:** File not found: jsonschema/simple_string.py + ```python + # generated by datamodel-codegen: + # filename: simple_string.json + + from __future__ import annotations + + from pydantic import BaseModel + + + class Model(BaseModel): + s: str + ``` --- From ed273baf534bad5f879261a902ef3d2e4746751f Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 17:13:28 +0000 Subject: [PATCH 14/20] Add Schema Version Support to docs navigation --- zensical.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/zensical.toml b/zensical.toml index fc89c3dc0..d347a2149 100644 --- a/zensical.toml +++ b/zensical.toml @@ -29,6 +29,7 @@ nav = [ ]} ]}, { "Support data types" = "supported-data-types.md" }, + { "Schema Version Support" = "supported_formats.md" }, { "Usage" = [ { "Input Formats" = [ { "OpenAPI" = "openapi.md" }, From 50d8a6cfe98790b03b0efd865a9b9bb202a467a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:13:58 +0000 Subject: [PATCH 15/20] docs: update llms.txt files Generated by GitHub Actions --- docs/llms-full.txt | 153 +++++++++++++++++++++++++++++++++++++++++++++ docs/llms.txt | 1 + 2 files changed, 154 insertions(+) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index ac6577d0a..986ef1f1c 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -24050,6 +24050,159 @@ Below are the data types and features recognized by datamodel-code-generator for --- +# Schema Version Support + +Source: https://datamodel-code-generator.koxudaxi.dev/supported_formats/ + +This document describes the JSON Schema and OpenAPI versions supported by datamodel-code-generator. + +## Overview + +datamodel-code-generator supports multiple versions of JSON Schema and OpenAPI specifications. By default, the tool operates in **Lenient mode**, accepting all features regardless of version declarations. This ensures maximum compatibility with real-world schemas that often mix features from different versions. + +## JSON Schema Version Support + +### Supported Versions + +| Version | Spec URL | Notes | +|---------|----------|-------| +| Draft 4 | [json-schema.org/draft-04](https://json-schema.org/draft-04/json-schema-core) | `id`, `definitions` | +| Draft 6 | [json-schema.org/draft-06](https://json-schema.org/draft-06/json-schema-release-notes) | `$id`, const, boolean schemas | +| Draft 7 | [json-schema.org/draft-07](https://json-schema.org/draft-07/json-schema-release-notes) | if/then/else, readOnly/writeOnly | +| 2019-09 | [json-schema.org/draft/2019-09](https://json-schema.org/draft/2019-09/release-notes) | `$defs`, `$anchor` | +| 2020-12 | [json-schema.org/draft/2020-12](https://json-schema.org/draft/2020-12/release-notes) | `prefixItems`, null in type arrays | + +### Feature Compatibility Matrix + +| Feature | Draft 4 | Draft 6 | Draft 7 | 2019-09 | 2020-12 | +|---------|---------|---------|---------|---------|---------| +| **ID/Reference** | +| ID field | `id` | `$id` | `$id` | `$id` | `$id` | +| Definitions key | `definitions` | `definitions` | `definitions` | `$defs`* | `$defs` | +| **Type Features** | +| Boolean schemas | - | Yes | Yes | Yes | Yes | +| Null in type array | - | - | - | - | Yes | +| const | - | Yes | Yes | Yes | Yes | +| **Numeric Constraints** | +| exclusiveMinimum (number) | - (boolean) | Yes | Yes | Yes | Yes | +| exclusiveMaximum (number) | - (boolean) | Yes | Yes | Yes | Yes | +| **Array Features** | +| prefixItems | - | - | - | - | Yes | +| items (as array/tuple) | Yes | Yes | Yes | Yes | - (single only) | +| contains | - | Yes | Yes | Yes | Yes | +| **Conditional** | +| if/then/else | - | - | Yes | Yes | Yes | +| **Metadata** | +| readOnly | - | - | Yes | Yes | Yes | +| writeOnly | - | - | Yes | Yes | Yes | + +*2019-09 supports both `definitions` and `$defs` for backward compatibility. + +### Version Detection + +datamodel-code-generator automatically detects the JSON Schema version: + +1. **Explicit `$schema` field**: If present, the version is detected from the URL pattern +2. **Heuristics**: If no `$schema`, presence of `$defs` suggests 2020-12, `definitions` suggests Draft 7 +3. **Fallback**: Draft 7 (most widely used) + +## OpenAPI Version Support + +### Supported Versions + +| Version | Spec URL | JSON Schema Base | +|---------|----------|------------------| +| 3.0.x | [spec.openapis.org/oas/v3.0.3](https://spec.openapis.org/oas/v3.0.3) | Draft 5 (subset) | +| 3.1.x | [spec.openapis.org/oas/v3.1.0](https://spec.openapis.org/oas/v3.1.0) | 2020-12 (full) | + +> **Note**: OpenAPI 2.0 (Swagger) support is limited. We recommend converting to OpenAPI 3.0+. + +### Feature Compatibility Matrix + +| Feature | OAS 3.0 | OAS 3.1 | +|---------|---------|---------| +| **Schema Base** | JSON Schema Draft 5 (subset) | JSON Schema 2020-12 (full) | +| **Definitions Path** | `#/components/schemas` | `#/components/schemas` | +| **Nullable** | +| `nullable: true` keyword | Yes | Deprecated | +| Null in type array | - | Yes | +| Type as array | - | Yes | +| **Array Features** | +| prefixItems | - | Yes | +| **Boolean Schemas** | - | Yes | +| **OpenAPI Specific** | +| discriminator | Yes | Yes | +| binary format | Yes | Yes | +| password format | Yes | Yes | +| webhooks | - | Yes | + +### Version Detection + +datamodel-code-generator detects the OpenAPI version from the `openapi` field: + +- `openapi: "3.0.x"` -> OpenAPI 3.0 +- `openapi: "3.1.x"` -> OpenAPI 3.1 +- No `openapi` field -> Fallback to OpenAPI 3.1 + +## Data Format Support + +### Common Formats (JSON Schema + OpenAPI) + +| Type | Format | Python Type | +|------|--------|-------------| +| integer | int32 | `int` | +| integer | int64 | `int` | +| number | float | `float` | +| number | double | `float` | +| number | decimal | `Decimal` | +| string | date | `date` | +| string | date-time | `datetime` | +| string | time | `time` | +| string | duration | `timedelta` | +| string | email | `EmailStr` | +| string | uri | `AnyUrl` | +| string | uuid | `UUID` | +| string | byte | `bytes` (base64) | +| string | ipv4 | `IPv4Address` | +| string | ipv6 | `IPv6Address` | +| string | hostname | `str` | + +### OpenAPI-Only Formats + +| Type | Format | Python Type | Notes | +|------|--------|-------------|-------| +| string | binary | `bytes` | File content | +| string | password | `SecretStr` | Sensitive data | + +## Limitations and Known Issues + +### JSON Schema + +1. **`$anchor` and `$dynamicRef`**: Not fully implemented +2. **`unevaluatedProperties`/`unevaluatedItems`**: Not implemented +3. **`contentMediaType`/`contentEncoding`**: Not implemented + +### OpenAPI + +1. **OpenAPI 2.0 (Swagger)**: Limited support, recommend converting to 3.0+ +2. **`$ref` sibling keywords in 3.0**: Not supported (3.0 spec limitation) + +### Mixed Version Schemas + +Real-world schemas often mix features from different versions. datamodel-code-generator handles this in **Lenient mode** (default): + +- Features from all versions are accepted +- No warnings for version mismatches +- Maximum compatibility with existing schemas + +## See Also + +- [Supported Data Types](./supported-data-types.md) - Complete data type support +- [JSON Schema Guide](./jsonschema.md) - JSON Schema usage examples +- [OpenAPI Guide](./openapi.md) - OpenAPI usage examples + +--- + # Generate from OpenAPI Source: https://datamodel-code-generator.koxudaxi.dev/openapi/ diff --git a/docs/llms.txt b/docs/llms.txt index 7839e2594..ab781966a 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -27,6 +27,7 @@ - [Supported Input Formats](https://datamodel-code-generator.koxudaxi.dev/supported-data-types/): This code generator supports the following input formats: +- [Schema Version Support](https://datamodel-code-generator.koxudaxi.dev/supported_formats/): This document describes the JSON Schema and OpenAPI versions supported by datamodel-code-generator. ## Usage ### Input Formats From 8078f37ee7b563a176b4b7060892b2caf44f4615 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 17:17:13 +0000 Subject: [PATCH 16/20] Docs: add detailed unsupported features tables --- docs/supported_formats.md | 62 ++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/docs/supported_formats.md b/docs/supported_formats.md index ca13172e9..066c085f6 100644 --- a/docs/supported_formats.md +++ b/docs/supported_formats.md @@ -122,16 +122,56 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field: ## Limitations and Known Issues -### JSON Schema - -1. **`$anchor` and `$dynamicRef`**: Not fully implemented -2. **`unevaluatedProperties`/`unevaluatedItems`**: Not implemented -3. **`contentMediaType`/`contentEncoding`**: Not implemented - -### OpenAPI - -1. **OpenAPI 2.0 (Swagger)**: Limited support, recommend converting to 3.0+ -2. **`$ref` sibling keywords in 3.0**: Not supported (3.0 spec limitation) +### JSON Schema - Unsupported Features + +| Feature | Status | Notes | +|---------|--------|-------| +| `$anchor` | ❌ Not supported | Use `$ref` with `$id` instead | +| `$dynamicRef` / `$dynamicAnchor` | ❌ Not supported | Draft 2020-12 dynamic references | +| `unevaluatedProperties` | ❌ Not supported | Use `additionalProperties` instead | +| `unevaluatedItems` | ❌ Not supported | Use `additionalItems` instead | +| `contentMediaType` | ❌ Not supported | Content type hints ignored | +| `contentEncoding` | ❌ Not supported | Encoding hints ignored | +| `contentSchema` | ❌ Not supported | Nested content schema ignored | +| `$vocabulary` | ❌ Not supported | Vocabulary declarations ignored | +| `$comment` | ⚠️ Ignored | Comments not preserved in output | +| `deprecated` | ⚠️ Partial | Recognized but not enforced | +| `examples` (array) | ⚠️ Partial | Only first example used for Field default | +| Recursive `$ref` | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | +| `propertyNames` | ❌ Not supported | Property name validation ignored | +| `dependentRequired` | ❌ Not supported | Dependent requirements ignored | +| `dependentSchemas` | ❌ Not supported | Dependent schemas ignored | + +### OpenAPI - Unsupported Features + +| Feature | Status | Notes | +|---------|--------|-------| +| OpenAPI 2.0 (Swagger) | ⚠️ Limited | Recommend converting to 3.0+ | +| `$ref` sibling keywords (3.0) | ❌ Not supported | 3.0 spec limitation | +| `links` | ❌ Not supported | Runtime linking not applicable | +| `callbacks` | ❌ Not supported | Webhook callbacks ignored | +| `security` definitions | ❌ Not supported | Security schemes not generated | +| `servers` | ❌ Not supported | Server configuration ignored | +| `externalDocs` | ❌ Not supported | External documentation links ignored | +| `xml` | ❌ Not supported | XML serialization hints ignored | +| Request body `required` | ⚠️ Partial | Affects field optionality | +| Header/Cookie parameters | ⚠️ Partial | Generated but not validated | + +### GraphQL - Unsupported Features + +| Feature | Status | Notes | +|---------|--------|-------| +| Directives | ❌ Not supported | Custom directives ignored | +| Subscriptions | ❌ Not supported | Only Query/Mutation types | +| Custom scalars | ⚠️ Partial | Mapped to `Any` by default | +| Interfaces inheritance | ⚠️ Partial | Flattened to concrete types | +| Federation directives | ❌ Not supported | Apollo Federation not supported | + +### Legend + +- ✅ Fully supported +- ⚠️ Partial support or limitations +- ❌ Not supported ### Mixed Version Schemas @@ -141,6 +181,8 @@ Real-world schemas often mix features from different versions. datamodel-code-ge - No warnings for version mismatches - Maximum compatibility with existing schemas +In **Strict mode** (`--schema-version-mode strict`), warnings are emitted for version-incompatible features. + ## See Also - [Supported Data Types](./supported-data-types.md) - Complete data type support From e93346c4a1605449048e826138774c7e9faf5d02 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:18:05 +0000 Subject: [PATCH 17/20] docs: update llms.txt files Generated by GitHub Actions --- docs/llms-full.txt | 62 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 986ef1f1c..207f03f37 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -24176,16 +24176,56 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field: ## Limitations and Known Issues -### JSON Schema - -1. **`$anchor` and `$dynamicRef`**: Not fully implemented -2. **`unevaluatedProperties`/`unevaluatedItems`**: Not implemented -3. **`contentMediaType`/`contentEncoding`**: Not implemented - -### OpenAPI - -1. **OpenAPI 2.0 (Swagger)**: Limited support, recommend converting to 3.0+ -2. **`$ref` sibling keywords in 3.0**: Not supported (3.0 spec limitation) +### JSON Schema - Unsupported Features + +| Feature | Status | Notes | +|---------|--------|-------| +| `$anchor` | ❌ Not supported | Use `$ref` with `$id` instead | +| `$dynamicRef` / `$dynamicAnchor` | ❌ Not supported | Draft 2020-12 dynamic references | +| `unevaluatedProperties` | ❌ Not supported | Use `additionalProperties` instead | +| `unevaluatedItems` | ❌ Not supported | Use `additionalItems` instead | +| `contentMediaType` | ❌ Not supported | Content type hints ignored | +| `contentEncoding` | ❌ Not supported | Encoding hints ignored | +| `contentSchema` | ❌ Not supported | Nested content schema ignored | +| `$vocabulary` | ❌ Not supported | Vocabulary declarations ignored | +| `$comment` | ⚠️ Ignored | Comments not preserved in output | +| `deprecated` | ⚠️ Partial | Recognized but not enforced | +| `examples` (array) | ⚠️ Partial | Only first example used for Field default | +| Recursive `$ref` | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | +| `propertyNames` | ❌ Not supported | Property name validation ignored | +| `dependentRequired` | ❌ Not supported | Dependent requirements ignored | +| `dependentSchemas` | ❌ Not supported | Dependent schemas ignored | + +### OpenAPI - Unsupported Features + +| Feature | Status | Notes | +|---------|--------|-------| +| OpenAPI 2.0 (Swagger) | ⚠️ Limited | Recommend converting to 3.0+ | +| `$ref` sibling keywords (3.0) | ❌ Not supported | 3.0 spec limitation | +| `links` | ❌ Not supported | Runtime linking not applicable | +| `callbacks` | ❌ Not supported | Webhook callbacks ignored | +| `security` definitions | ❌ Not supported | Security schemes not generated | +| `servers` | ❌ Not supported | Server configuration ignored | +| `externalDocs` | ❌ Not supported | External documentation links ignored | +| `xml` | ❌ Not supported | XML serialization hints ignored | +| Request body `required` | ⚠️ Partial | Affects field optionality | +| Header/Cookie parameters | ⚠️ Partial | Generated but not validated | + +### GraphQL - Unsupported Features + +| Feature | Status | Notes | +|---------|--------|-------| +| Directives | ❌ Not supported | Custom directives ignored | +| Subscriptions | ❌ Not supported | Only Query/Mutation types | +| Custom scalars | ⚠️ Partial | Mapped to `Any` by default | +| Interfaces inheritance | ⚠️ Partial | Flattened to concrete types | +| Federation directives | ❌ Not supported | Apollo Federation not supported | + +### Legend + +- ✅ Fully supported +- ⚠️ Partial support or limitations +- ❌ Not supported ### Mixed Version Schemas @@ -24195,6 +24235,8 @@ Real-world schemas often mix features from different versions. datamodel-code-ge - No warnings for version mismatches - Maximum compatibility with existing schemas +In **Strict mode** (`--schema-version-mode strict`), warnings are emitted for version-incompatible features. + ## See Also - [Supported Data Types](./supported-data-types.md) - Complete data type support From da95f2d71da9ba7d1601087a5a9a2984e9b50624 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 17:20:32 +0000 Subject: [PATCH 18/20] Docs: add version info to unsupported features tables --- docs/supported_formats.md | 74 ++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/docs/supported_formats.md b/docs/supported_formats.md index 066c085f6..ccc8d2c10 100644 --- a/docs/supported_formats.md +++ b/docs/supported_formats.md @@ -124,48 +124,50 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field: ### JSON Schema - Unsupported Features -| Feature | Status | Notes | -|---------|--------|-------| -| `$anchor` | ❌ Not supported | Use `$ref` with `$id` instead | -| `$dynamicRef` / `$dynamicAnchor` | ❌ Not supported | Draft 2020-12 dynamic references | -| `unevaluatedProperties` | ❌ Not supported | Use `additionalProperties` instead | -| `unevaluatedItems` | ❌ Not supported | Use `additionalItems` instead | -| `contentMediaType` | ❌ Not supported | Content type hints ignored | -| `contentEncoding` | ❌ Not supported | Encoding hints ignored | -| `contentSchema` | ❌ Not supported | Nested content schema ignored | -| `$vocabulary` | ❌ Not supported | Vocabulary declarations ignored | -| `$comment` | ⚠️ Ignored | Comments not preserved in output | -| `deprecated` | ⚠️ Partial | Recognized but not enforced | -| `examples` (array) | ⚠️ Partial | Only first example used for Field default | -| Recursive `$ref` | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | -| `propertyNames` | ❌ Not supported | Property name validation ignored | -| `dependentRequired` | ❌ Not supported | Dependent requirements ignored | -| `dependentSchemas` | ❌ Not supported | Dependent schemas ignored | +| Feature | Introduced | Status | Notes | +|---------|------------|--------|-------| +| `$anchor` | 2019-09 | ❌ Not supported | Use `$ref` with `$id` instead | +| `$dynamicRef` / `$dynamicAnchor` | 2020-12 | ❌ Not supported | Dynamic references | +| `unevaluatedProperties` | 2019-09 | ❌ Not supported | Use `additionalProperties` instead | +| `unevaluatedItems` | 2019-09 | ❌ Not supported | Use `additionalItems` instead | +| `contentMediaType` | Draft 7 | ❌ Not supported | Content type hints ignored | +| `contentEncoding` | Draft 7 | ❌ Not supported | Encoding hints ignored | +| `contentSchema` | 2019-09 | ❌ Not supported | Nested content schema ignored | +| `$vocabulary` | 2019-09 | ❌ Not supported | Vocabulary declarations ignored | +| `$comment` | Draft 7 | ⚠️ Ignored | Comments not preserved in output | +| `deprecated` | 2019-09 | ⚠️ Partial | Recognized but not enforced | +| `examples` (array) | Draft 6 | ⚠️ Partial | Only first example used for Field default | +| Recursive `$ref` | Draft 4+ | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | +| `propertyNames` | Draft 6 | ❌ Not supported | Property name validation ignored | +| `dependentRequired` | 2019-09 | ❌ Not supported | Dependent requirements ignored | +| `dependentSchemas` | 2019-09 | ❌ Not supported | Dependent schemas ignored | ### OpenAPI - Unsupported Features -| Feature | Status | Notes | -|---------|--------|-------| -| OpenAPI 2.0 (Swagger) | ⚠️ Limited | Recommend converting to 3.0+ | -| `$ref` sibling keywords (3.0) | ❌ Not supported | 3.0 spec limitation | -| `links` | ❌ Not supported | Runtime linking not applicable | -| `callbacks` | ❌ Not supported | Webhook callbacks ignored | -| `security` definitions | ❌ Not supported | Security schemes not generated | -| `servers` | ❌ Not supported | Server configuration ignored | -| `externalDocs` | ❌ Not supported | External documentation links ignored | -| `xml` | ❌ Not supported | XML serialization hints ignored | -| Request body `required` | ⚠️ Partial | Affects field optionality | -| Header/Cookie parameters | ⚠️ Partial | Generated but not validated | +| Feature | Introduced | Status | Notes | +|---------|------------|--------|-------| +| OpenAPI 2.0 (Swagger) | OAS 2.0 | ⚠️ Limited | Recommend converting to 3.0+ | +| `$ref` sibling keywords | OAS 3.0 | ❌ Not supported | 3.0 spec limitation (fixed in 3.1) | +| `links` | OAS 3.0 | ❌ Not supported | Runtime linking not applicable | +| `callbacks` | OAS 3.0 | ❌ Not supported | Webhook callbacks ignored | +| `webhooks` | OAS 3.1 | ❌ Not supported | Top-level webhooks ignored | +| `security` definitions | OAS 2.0+ | ❌ Not supported | Security schemes not generated | +| `servers` | OAS 3.0 | ❌ Not supported | Server configuration ignored | +| `externalDocs` | OAS 2.0+ | ❌ Not supported | External documentation links ignored | +| `xml` | OAS 2.0+ | ❌ Not supported | XML serialization hints ignored | +| Request body `required` | OAS 3.0 | ⚠️ Partial | Affects field optionality | +| Header/Cookie parameters | OAS 3.0 | ⚠️ Partial | Generated but not validated | ### GraphQL - Unsupported Features -| Feature | Status | Notes | -|---------|--------|-------| -| Directives | ❌ Not supported | Custom directives ignored | -| Subscriptions | ❌ Not supported | Only Query/Mutation types | -| Custom scalars | ⚠️ Partial | Mapped to `Any` by default | -| Interfaces inheritance | ⚠️ Partial | Flattened to concrete types | -| Federation directives | ❌ Not supported | Apollo Federation not supported | +| Feature | Spec | Status | Notes | +|---------|------|--------|-------| +| Directives | Core | ❌ Not supported | Custom directives ignored | +| Subscriptions | Core | ❌ Not supported | Only Query/Mutation types | +| Custom scalars | Core | ⚠️ Partial | Mapped to `Any` by default | +| Interfaces inheritance | Core | ⚠️ Partial | Flattened to concrete types | +| Federation directives | Apollo | ❌ Not supported | Apollo Federation not supported | +| Input unions | Proposal | ❌ Not supported | Not yet in GraphQL spec | ### Legend From 35bd40fa95aaaa4f90ca075d2f74c3e4125a167a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 17:22:06 +0000 Subject: [PATCH 19/20] docs: update llms.txt files Generated by GitHub Actions --- docs/llms-full.txt | 74 ++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 207f03f37..6f7d1522b 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -24178,48 +24178,50 @@ datamodel-code-generator detects the OpenAPI version from the `openapi` field: ### JSON Schema - Unsupported Features -| Feature | Status | Notes | -|---------|--------|-------| -| `$anchor` | ❌ Not supported | Use `$ref` with `$id` instead | -| `$dynamicRef` / `$dynamicAnchor` | ❌ Not supported | Draft 2020-12 dynamic references | -| `unevaluatedProperties` | ❌ Not supported | Use `additionalProperties` instead | -| `unevaluatedItems` | ❌ Not supported | Use `additionalItems` instead | -| `contentMediaType` | ❌ Not supported | Content type hints ignored | -| `contentEncoding` | ❌ Not supported | Encoding hints ignored | -| `contentSchema` | ❌ Not supported | Nested content schema ignored | -| `$vocabulary` | ❌ Not supported | Vocabulary declarations ignored | -| `$comment` | ⚠️ Ignored | Comments not preserved in output | -| `deprecated` | ⚠️ Partial | Recognized but not enforced | -| `examples` (array) | ⚠️ Partial | Only first example used for Field default | -| Recursive `$ref` | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | -| `propertyNames` | ❌ Not supported | Property name validation ignored | -| `dependentRequired` | ❌ Not supported | Dependent requirements ignored | -| `dependentSchemas` | ❌ Not supported | Dependent schemas ignored | +| Feature | Introduced | Status | Notes | +|---------|------------|--------|-------| +| `$anchor` | 2019-09 | ❌ Not supported | Use `$ref` with `$id` instead | +| `$dynamicRef` / `$dynamicAnchor` | 2020-12 | ❌ Not supported | Dynamic references | +| `unevaluatedProperties` | 2019-09 | ❌ Not supported | Use `additionalProperties` instead | +| `unevaluatedItems` | 2019-09 | ❌ Not supported | Use `additionalItems` instead | +| `contentMediaType` | Draft 7 | ❌ Not supported | Content type hints ignored | +| `contentEncoding` | Draft 7 | ❌ Not supported | Encoding hints ignored | +| `contentSchema` | 2019-09 | ❌ Not supported | Nested content schema ignored | +| `$vocabulary` | 2019-09 | ❌ Not supported | Vocabulary declarations ignored | +| `$comment` | Draft 7 | ⚠️ Ignored | Comments not preserved in output | +| `deprecated` | 2019-09 | ⚠️ Partial | Recognized but not enforced | +| `examples` (array) | Draft 6 | ⚠️ Partial | Only first example used for Field default | +| Recursive `$ref` | Draft 4+ | ⚠️ Partial | Supported with `ForwardRef`, may require manual adjustment | +| `propertyNames` | Draft 6 | ❌ Not supported | Property name validation ignored | +| `dependentRequired` | 2019-09 | ❌ Not supported | Dependent requirements ignored | +| `dependentSchemas` | 2019-09 | ❌ Not supported | Dependent schemas ignored | ### OpenAPI - Unsupported Features -| Feature | Status | Notes | -|---------|--------|-------| -| OpenAPI 2.0 (Swagger) | ⚠️ Limited | Recommend converting to 3.0+ | -| `$ref` sibling keywords (3.0) | ❌ Not supported | 3.0 spec limitation | -| `links` | ❌ Not supported | Runtime linking not applicable | -| `callbacks` | ❌ Not supported | Webhook callbacks ignored | -| `security` definitions | ❌ Not supported | Security schemes not generated | -| `servers` | ❌ Not supported | Server configuration ignored | -| `externalDocs` | ❌ Not supported | External documentation links ignored | -| `xml` | ❌ Not supported | XML serialization hints ignored | -| Request body `required` | ⚠️ Partial | Affects field optionality | -| Header/Cookie parameters | ⚠️ Partial | Generated but not validated | +| Feature | Introduced | Status | Notes | +|---------|------------|--------|-------| +| OpenAPI 2.0 (Swagger) | OAS 2.0 | ⚠️ Limited | Recommend converting to 3.0+ | +| `$ref` sibling keywords | OAS 3.0 | ❌ Not supported | 3.0 spec limitation (fixed in 3.1) | +| `links` | OAS 3.0 | ❌ Not supported | Runtime linking not applicable | +| `callbacks` | OAS 3.0 | ❌ Not supported | Webhook callbacks ignored | +| `webhooks` | OAS 3.1 | ❌ Not supported | Top-level webhooks ignored | +| `security` definitions | OAS 2.0+ | ❌ Not supported | Security schemes not generated | +| `servers` | OAS 3.0 | ❌ Not supported | Server configuration ignored | +| `externalDocs` | OAS 2.0+ | ❌ Not supported | External documentation links ignored | +| `xml` | OAS 2.0+ | ❌ Not supported | XML serialization hints ignored | +| Request body `required` | OAS 3.0 | ⚠️ Partial | Affects field optionality | +| Header/Cookie parameters | OAS 3.0 | ⚠️ Partial | Generated but not validated | ### GraphQL - Unsupported Features -| Feature | Status | Notes | -|---------|--------|-------| -| Directives | ❌ Not supported | Custom directives ignored | -| Subscriptions | ❌ Not supported | Only Query/Mutation types | -| Custom scalars | ⚠️ Partial | Mapped to `Any` by default | -| Interfaces inheritance | ⚠️ Partial | Flattened to concrete types | -| Federation directives | ❌ Not supported | Apollo Federation not supported | +| Feature | Spec | Status | Notes | +|---------|------|--------|-------| +| Directives | Core | ❌ Not supported | Custom directives ignored | +| Subscriptions | Core | ❌ Not supported | Only Query/Mutation types | +| Custom scalars | Core | ⚠️ Partial | Mapped to `Any` by default | +| Interfaces inheritance | Core | ⚠️ Partial | Flattened to concrete types | +| Federation directives | Apollo | ❌ Not supported | Apollo Federation not supported | +| Input unions | Proposal | ❌ Not supported | Not yet in GraphQL spec | ### Legend From 332cccdd61f7012880a655f46a3812c46482a484 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 17:47:37 +0000 Subject: [PATCH 20/20] Add e2e tests for schema version error handling and strict mode warnings --- .../parser/jsonschema.py | 3 - tests/parser/test_base.py | 2 + tests/parser/test_schema_version.py | 129 ++++++++++++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index b15b4cfca..db50baf94 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -3668,9 +3668,6 @@ def _check_version_specific_features( ) return - if not isinstance(raw, dict): - return - # Check null in type array (Draft 2020-12 / OpenAPI 3.1+) type_value = raw.get("type") if isinstance(type_value, list) and "null" in type_value and not self.schema_features.null_in_type_array: diff --git a/tests/parser/test_base.py b/tests/parser/test_base.py index 37ea386bf..61facdef9 100644 --- a/tests/parser/test_base.py +++ b/tests/parser/test_base.py @@ -65,6 +65,8 @@ def test_parser() -> None: assert c.data_model_root_type == B assert c.data_model_field_type == DataModelFieldBase assert c.base_class == "Base" + # Test schema_features property of test stub + assert c.schema_features.prefix_items is True def test_add_model_path_to_list() -> None: diff --git a/tests/parser/test_schema_version.py b/tests/parser/test_schema_version.py index 0cc0f4afb..ca0d37f1a 100644 --- a/tests/parser/test_schema_version.py +++ b/tests/parser/test_schema_version.py @@ -952,3 +952,132 @@ def test_cli_schema_version_mode_parametrized(version_mode: VersionMode) -> None class Model(BaseModel): s: str""" ) + + +# ============================================================================= +# Error handling tests for invalid schema versions +# ============================================================================= + + +def test_invalid_jsonschema_version_error() -> None: + """Test that invalid JSON Schema version raises Error.""" + from datamodel_code_generator import Error, generate + + with pytest.raises(Error) as exc_info: + generate( + JSON_SCHEMA_DATA_PATH / "simple_string.json", + input_file_type=datamodel_code_generator.InputFileType.JsonSchema, + schema_version="invalid-version", + ) + assert "Invalid JSON Schema version" in str(exc_info.value) + assert "invalid-version" in str(exc_info.value) + + +def test_invalid_openapi_version_error() -> None: + """Test that invalid OpenAPI version raises Error.""" + from datamodel_code_generator import Error, generate + + with pytest.raises(Error) as exc_info: + generate( + OPENAPI_DATA_PATH / "api.yaml", + input_file_type=datamodel_code_generator.InputFileType.OpenAPI, + schema_version="invalid-version", + ) + assert "Invalid OpenAPI version" in str(exc_info.value) + assert "invalid-version" in str(exc_info.value) + + +def test_graphql_schema_version_not_supported() -> None: + """Test that --schema-version is not supported for GraphQL.""" + from datamodel_code_generator import Error, generate + + graphql_data_path = Path(__file__).parent.parent / "data" / "graphql" + + with pytest.raises(Error) as exc_info: + generate( + graphql_data_path / "schema.graphql", + input_file_type=datamodel_code_generator.InputFileType.GraphQL, + schema_version="draft-07", + ) + assert "--schema-version is not supported" in str(exc_info.value) + assert "graphql" in str(exc_info.value).lower() + + +# ============================================================================= +# E2E tests for strict mode warnings +# ============================================================================= + + +def test_e2e_exclusive_maximum_as_bool_strict_warning_draft7() -> None: + """Test that boolean exclusiveMaximum emits warning in Draft 7 Strict mode via generate().""" + import json + import tempfile + import warnings + + from datamodel_code_generator import generate + + # Draft 4 style schema with boolean exclusiveMaximum in definitions + schema = { + "type": "object", + "definitions": { + "MyValue": { + "type": "number", + "maximum": 10, + "exclusiveMaximum": True, + } + }, + "properties": {"value": {"$ref": "#/definitions/MyValue"}}, + } + + with tempfile.NamedTemporaryFile(encoding="utf-8", mode="w", suffix=".json", delete=False) as f: + json.dump(schema, f) + f.flush() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = generate( + Path(f.name), + input_file_type=datamodel_code_generator.InputFileType.JsonSchema, + schema_version="draft-07", + schema_version_mode=VersionMode.Strict, + ) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert any("exclusiveMaximum as boolean" in str(uw.message) for uw in user_warnings) + assert result is not None + + +def test_e2e_exclusive_maximum_as_number_strict_warning_draft4() -> None: + """Test that numeric exclusiveMaximum emits warning in Draft 4 Strict mode via generate().""" + import json + import tempfile + import warnings + + from datamodel_code_generator import generate + + # Draft 6+ style schema with numeric exclusiveMaximum in definitions + schema = { + "type": "object", + "definitions": { + "MyValue": { + "type": "number", + "exclusiveMaximum": 10, + } + }, + "properties": {"value": {"$ref": "#/definitions/MyValue"}}, + } + + with tempfile.NamedTemporaryFile(encoding="utf-8", mode="w", suffix=".json", delete=False) as f: + json.dump(schema, f) + f.flush() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = generate( + Path(f.name), + input_file_type=datamodel_code_generator.InputFileType.JsonSchema, + schema_version="draft-04", + schema_version_mode=VersionMode.Strict, + ) + user_warnings = [x for x in w if issubclass(x.category, UserWarning)] + assert any("exclusiveMaximum as number" in str(uw.message) for uw in user_warnings) + assert result is not None