Skip to content

Commit e0ad9a6

Browse files
committed
Add --schema-version and --schema-version-mode CLI options
1 parent 7ee780c commit e0ad9a6

10 files changed

Lines changed: 149 additions & 19 deletions

File tree

src/datamodel_code_generator/__init__.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@
6565
from datamodel_code_generator.parser import DefaultPutDict, LiteralType
6666

6767
if TYPE_CHECKING:
68-
from datamodel_code_generator._types import GraphQLParserConfigDict, OpenAPIParserConfigDict, ParserConfigDict
68+
from datamodel_code_generator._types import (
69+
GraphQLParserConfigDict,
70+
JSONSchemaParserConfigDict,
71+
OpenAPIParserConfigDict,
72+
ParserConfigDict,
73+
)
6974
from datamodel_code_generator._types.generate_config_dict import GenerateConfigDict
7075
from datamodel_code_generator.config import GenerateConfig, ParserConfig
7176

@@ -456,7 +461,10 @@ def _build_module_content(
456461
def _create_parser_config(
457462
config_class: type[_ConfigT],
458463
generate_config: GenerateConfig,
459-
additional_options: ParserConfigDict | OpenAPIParserConfigDict | GraphQLParserConfigDict,
464+
additional_options: ParserConfigDict
465+
| JSONSchemaParserConfigDict
466+
| OpenAPIParserConfigDict
467+
| GraphQLParserConfigDict,
460468
) -> _ConfigT:
461469
"""Create a parser config from GenerateConfig with additional options.
462470
@@ -735,6 +743,28 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
735743
),
736744
}
737745

746+
# Convert schema_version string to appropriate enum based on input type
747+
jsonschema_version: JsonSchemaVersion | None = None
748+
openapi_version: OpenAPIVersion | None = None
749+
if config.schema_version and config.schema_version != "auto":
750+
if input_file_type == InputFileType.OpenAPI:
751+
try:
752+
openapi_version = OpenAPIVersion(config.schema_version)
753+
except ValueError:
754+
valid = [v.value for v in OpenAPIVersion]
755+
msg = f"Invalid OpenAPI version: {config.schema_version}. Valid values: {valid}"
756+
raise Error(msg) from None
757+
elif input_file_type == InputFileType.GraphQL:
758+
msg = f"--schema-version is not supported for {input_file_type.value}"
759+
raise Error(msg)
760+
else:
761+
try:
762+
jsonschema_version = JsonSchemaVersion(config.schema_version)
763+
except ValueError:
764+
valid = [v.value for v in JsonSchemaVersion]
765+
msg = f"Invalid JSON Schema version: {config.schema_version}. Valid values: {valid}"
766+
raise Error(msg) from None
767+
738768
if input_file_type == InputFileType.OpenAPI:
739769
from datamodel_code_generator.parser.openapi import OpenAPIParser # noqa: PLC0415
740770

@@ -743,6 +773,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
743773
"include_path_parameters": config.include_path_parameters,
744774
"use_status_code_in_response_name": config.use_status_code_in_response_name,
745775
"openapi_include_paths": config.openapi_include_paths,
776+
"openapi_version": openapi_version,
746777
**additional_options,
747778
}
748779
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]:
760791
else:
761792
from datamodel_code_generator.parser.jsonschema import JsonSchemaParser # noqa: PLC0415
762793

763-
parser_config = _create_parser_config(JSONSchemaParserConfig, config, additional_options)
794+
jsonschema_additional_options: JSONSchemaParserConfigDict = {
795+
"jsonschema_version": jsonschema_version,
796+
**additional_options,
797+
}
798+
parser_config = _create_parser_config(JSONSchemaParserConfig, config, jsonschema_additional_options)
764799
parser = JsonSchemaParser(source=source, config=parser_config) # ty: ignore
765800

766801
with chdir(config.output):

src/datamodel_code_generator/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
ReadOnlyWriteOnlyModelType,
7070
ReuseScope,
7171
TargetPydanticVersion,
72+
VersionMode,
7273
enable_debug_message,
7374
generate,
7475
)
@@ -620,6 +621,8 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) ->
620621
module_split_mode: Optional[ModuleSplitMode] = None # noqa: UP045
621622
watch: bool = False
622623
watch_delay: float = 0.5
624+
schema_version: Optional[str] = None # noqa: UP045
625+
schema_version_mode: Optional[VersionMode] = None # noqa: UP045
623626

624627
def merge_args(self, args: Namespace) -> None:
625628
"""Merge command-line arguments into config."""
@@ -1063,6 +1066,8 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
10631066
module_split_mode=config.module_split_mode,
10641067
validators=validators,
10651068
default_value_overrides=default_value_overrides,
1069+
schema_version=config.schema_version,
1070+
schema_version_mode=config.schema_version_mode,
10661071
)
10671072

10681073
if output is None and result is not None: # pragma: no cover

src/datamodel_code_generator/_types/generate_config_dict.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
StrictTypes,
3333
TargetPydanticVersion,
3434
UnionMode,
35+
VersionMode,
3536
)
3637
from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion
3738
from datamodel_code_generator.parser import LiteralType
@@ -168,6 +169,8 @@ class GenerateConfigDict(TypedDict, closed=True):
168169
field_type_collision_strategy: NotRequired[FieldTypeCollisionStrategy | None]
169170
module_split_mode: NotRequired[ModuleSplitMode | None]
170171
default_value_overrides: NotRequired[Mapping[str, Any] | None]
172+
schema_version: NotRequired[str | None]
173+
schema_version_mode: NotRequired[VersionMode | None]
171174

172175

173176
class ValidatorDefinition(TypedDict):

src/datamodel_code_generator/arguments.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,15 @@
2626
FieldTypeCollisionStrategy,
2727
InputFileType,
2828
InputModelRefStrategy,
29-
JsonSchemaVersion,
3029
ModuleSplitMode,
3130
NamingStrategy,
3231
OpenAPIScope,
33-
OpenAPIVersion,
3432
ReadOnlyWriteOnlyModelType,
3533
ReuseScope,
3634
StrictTypes,
3735
TargetPydanticVersion,
3836
UnionMode,
37+
VersionMode,
3938
)
4039
from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion
4140
from datamodel_code_generator.parser import LiteralType
@@ -986,22 +985,23 @@ def start_section(self, heading: str | None) -> None:
986985
action="store_true",
987986
default=None,
988987
)
989-
openapi_options.add_argument(
990-
"--openapi-version",
991-
help="OpenAPI version to use (default: auto-detect from 'openapi' field). "
992-
"Use 'auto' for auto-detection, '3.0' for OpenAPI 3.0.x, '3.1' for OpenAPI 3.1.x.",
993-
choices=[v.value for v in OpenAPIVersion],
994-
default=None,
995-
)
996-
997988
# ======================================================================================
998-
# Options specific to JSON Schema input
989+
# Schema version options (for both JSON Schema and OpenAPI)
999990
# ======================================================================================
1000991
base_options.add_argument(
1001-
"--jsonschema-version",
1002-
help="JSON Schema version to use (default: auto-detect from '$schema' field). "
1003-
"Use 'auto' for auto-detection, or specify a draft version explicitly.",
1004-
choices=[v.value for v in JsonSchemaVersion],
992+
"--schema-version",
993+
help="Schema version. Valid values depend on input type: "
994+
"JsonSchema: auto, draft-04, draft-06, draft-07, 2019-09, 2020-12. "
995+
"OpenAPI: auto, 3.0, 3.1. "
996+
"(default: auto - detected from $schema or openapi field)",
997+
default=None,
998+
)
999+
base_options.add_argument(
1000+
"--schema-version-mode",
1001+
help="Schema version validation mode. "
1002+
"'lenient': accept all features regardless of version (default). "
1003+
"'strict': warn on features outside declared/detected version.",
1004+
choices=[m.value for m in VersionMode],
10051005
default=None,
10061006
)
10071007

src/datamodel_code_generator/cli_options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class CLIOptionMeta:
7070
"--input-model-ref-strategy": CLIOptionMeta(name="--input-model-ref-strategy", category=OptionCategory.BASE),
7171
"--input-file-type": CLIOptionMeta(name="--input-file-type", category=OptionCategory.BASE),
7272
"--encoding": CLIOptionMeta(name="--encoding", category=OptionCategory.BASE),
73+
"--schema-version": CLIOptionMeta(name="--schema-version", category=OptionCategory.BASE),
74+
"--schema-version-mode": CLIOptionMeta(name="--schema-version-mode", category=OptionCategory.BASE),
7375
# ==========================================================================
7476
# Model Customization
7577
# ==========================================================================

src/datamodel_code_generator/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
ReadOnlyWriteOnlyModelType,
3131
ReuseScope,
3232
TargetPydanticVersion,
33+
VersionMode,
3334
)
3435
from datamodel_code_generator.format import (
3536
DateClassType,
@@ -207,6 +208,8 @@ class Config:
207208
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None
208209
module_split_mode: ModuleSplitMode | None = None
209210
default_value_overrides: Mapping[str, Any] | None = None
211+
schema_version: str | None = None
212+
schema_version_mode: VersionMode | None = None
210213

211214

212215
class ParserConfig(BaseModel):

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ class JsonSchemaParser(Parser["JSONSchemaParserConfig"]):
664664
})
665665

666666
@classmethod
667-
def _create_default_config(cls, options: JSONSchemaParserConfigDict) -> JSONSchemaParserConfig:
667+
def _create_default_config(cls, options: JSONSchemaParserConfigDict) -> JSONSchemaParserConfig: # type: ignore[override]
668668
"""Create a JSONSchemaParserConfig from options."""
669669
from datamodel_code_generator import types as types_module # noqa: PLC0415
670670
from datamodel_code_generator.config import JSONSchemaParserConfig # noqa: PLC0415

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ class DataclassArguments(TypedDict, closed=True):
114114
ValidatorMode: TypeAlias = Literal['before', 'after', 'wrap', 'plain']
115115

116116

117+
VersionMode: TypeAlias = Literal['lenient', 'strict']
118+
119+
117120
class GenerateConfig(TypedDict, closed=True):
118121
input_filename: NotRequired[str | None]
119122
input_file_type: NotRequired[InputFileType]
@@ -246,6 +249,8 @@ class GenerateConfig(TypedDict, closed=True):
246249
field_type_collision_strategy: NotRequired[FieldTypeCollisionStrategy | None]
247250
module_split_mode: NotRequired[ModuleSplitMode | None]
248251
default_value_overrides: NotRequired[Mapping[str, Any] | None]
252+
schema_version: NotRequired[str | None]
253+
schema_version_mode: NotRequired[VersionMode | None]
249254

250255

251256
class ValidatorDefinition(TypedDict):

tests/main/test_public_api_signature_baseline.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ReadOnlyWriteOnlyModelType,
2929
ReuseScope,
3030
TargetPydanticVersion,
31+
VersionMode,
3132
)
3233
from datamodel_code_generator.format import PythonVersionMin
3334
from datamodel_code_generator.model import DataModel, DataModelFieldBase
@@ -187,6 +188,8 @@ def _baseline_generate(
187188
all_exports_collision_strategy: AllExportsCollisionStrategy | None = None,
188189
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None,
189190
module_split_mode: ModuleSplitMode | None = None,
191+
schema_version: str | None = None,
192+
schema_version_mode: VersionMode | None = None,
190193
) -> str | object | None:
191194
raise NotImplementedError
192195

tests/parser/test_schema_version.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from pathlib import Path
6+
57
import pytest
68
from inline_snapshot import snapshot
79

@@ -14,6 +16,9 @@
1416
detect_openapi_version,
1517
)
1618

19+
# Path to test data
20+
JSON_SCHEMA_DATA_PATH = Path(__file__).parent.parent / "data" / "jsonschema"
21+
1722

1823
def test_detect_jsonschema_version_draft4() -> None:
1924
"""Test detection of Draft 4 from $schema field."""
@@ -421,3 +426,72 @@ def test_openapi_parser_schema_features_detection() -> None:
421426
features = parser.schema_features
422427
assert features.nullable_keyword == snapshot(False)
423428
assert features.null_in_type_array == snapshot(True)
429+
430+
431+
def test_jsonschema_parser_config_version_override() -> None:
432+
"""Test that JsonSchemaParser uses config version over auto-detection."""
433+
from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
434+
435+
parser = JsonSchemaParser("", jsonschema_version=JsonSchemaVersion.Draft4)
436+
parser.raw_obj = {"$schema": "http://json-schema.org/draft-07/schema#"}
437+
features = parser.schema_features
438+
assert features.id_field == snapshot("id")
439+
assert features.boolean_schemas == snapshot(False)
440+
441+
442+
def test_openapi_parser_config_version_override() -> None:
443+
"""Test that OpenAPIParser uses config version over auto-detection."""
444+
from datamodel_code_generator.parser.openapi import OpenAPIParser
445+
446+
parser = OpenAPIParser("", openapi_version=OpenAPIVersion.V30)
447+
parser.raw_obj = {"openapi": "3.1.0"}
448+
features = parser.schema_features
449+
assert features.nullable_keyword == snapshot(True)
450+
assert features.null_in_type_array == snapshot(False)
451+
452+
453+
@pytest.mark.cli_doc(
454+
options=["--schema-version"],
455+
option_description="""Schema version to use for parsing.
456+
457+
The `--schema-version` option specifies the schema version to use instead of auto-detection.
458+
Valid values depend on input type: JsonSchema (draft-04, draft-06, draft-07, 2019-09, 2020-12)
459+
or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field).""",
460+
input_schema="jsonschema/simple_string.json",
461+
cli_args=["--schema-version", "draft-07"],
462+
golden_output="jsonschema/simple_string.py",
463+
)
464+
def test_cli_schema_version_jsonschema() -> None:
465+
"""Test --schema-version option with JSON Schema input."""
466+
from datamodel_code_generator import generate
467+
468+
result = generate(
469+
JSON_SCHEMA_DATA_PATH / "simple_string.json",
470+
input_file_type=datamodel_code_generator.InputFileType.JsonSchema,
471+
schema_version="draft-07",
472+
)
473+
assert result is not None
474+
assert "class Model" in result or "Model" in result
475+
476+
477+
@pytest.mark.cli_doc(
478+
options=["--schema-version-mode"],
479+
option_description="""Schema version validation mode.
480+
481+
The `--schema-version-mode` option controls how schema version validation is performed.
482+
'lenient' (default): accept all features regardless of version.
483+
'strict': warn on features outside the declared/detected version.""",
484+
input_schema="jsonschema/simple_string.json",
485+
cli_args=["--schema-version-mode", "lenient"],
486+
golden_output="jsonschema/simple_string.py",
487+
)
488+
def test_cli_schema_version_mode() -> None:
489+
"""Test --schema-version-mode option."""
490+
from datamodel_code_generator import generate
491+
492+
result = generate(
493+
JSON_SCHEMA_DATA_PATH / "simple_string.json",
494+
input_file_type=datamodel_code_generator.InputFileType.JsonSchema,
495+
schema_version_mode=VersionMode.Lenient,
496+
)
497+
assert result is not None

0 commit comments

Comments
 (0)