Skip to content

Commit 68ffafa

Browse files
authored
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
1 parent 811cf13 commit 68ffafa

16 files changed

Lines changed: 770 additions & 24 deletions

File tree

docs/cli-reference/base-options.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
| [`--input-model`](#input-model) | Import a Python type or dict schema from a module. |
1111
| [`--input-model-ref-strategy`](#input-model-ref-strategy) | Strategy for referenced types when using --input-model. |
1212
| [`--output`](#output) | Specify the destination path for generated Python code. |
13+
| [`--schema-version`](#schema-version) | Schema version to use for parsing. |
14+
| [`--schema-version-mode`](#schema-version-mode) | Schema version validation mode. |
1315
| [`--url`](#url) | Fetch schema from URL with custom HTTP headers. |
1416

1517
---
@@ -326,6 +328,76 @@ is written to stdout.
326328

327329
---
328330

331+
## `--schema-version` {#schema-version}
332+
333+
Schema version to use for parsing.
334+
335+
The `--schema-version` option specifies the schema version to use instead of auto-detection.
336+
Valid values depend on input type: JsonSchema (draft-04, draft-06, draft-07, 2019-09, 2020-12)
337+
or OpenAPI (3.0, 3.1). Default is 'auto' (detected from $schema or openapi field).
338+
339+
!!! tip "Usage"
340+
341+
```bash
342+
datamodel-codegen --input schema.json --schema-version draft-07 # (1)!
343+
```
344+
345+
1. :material-arrow-left: `--schema-version` - the option documented here
346+
347+
??? example "Examples"
348+
349+
**Input Schema:**
350+
351+
```json
352+
{
353+
"$schema": "http://json-schema.org/draft-07/schema",
354+
"type": "object",
355+
"properties": {"s": {"type": ["string"]}},
356+
"required": ["s"]
357+
}
358+
```
359+
360+
**Output:**
361+
362+
> **Error:** File not found: jsonschema/simple_string.py
363+
364+
---
365+
366+
## `--schema-version-mode` {#schema-version-mode}
367+
368+
Schema version validation mode.
369+
370+
The `--schema-version-mode` option controls how schema version validation is performed.
371+
'lenient' (default): accept all features regardless of version.
372+
'strict': warn on features outside the declared/detected version.
373+
374+
!!! tip "Usage"
375+
376+
```bash
377+
datamodel-codegen --input schema.json --schema-version-mode lenient # (1)!
378+
```
379+
380+
1. :material-arrow-left: `--schema-version-mode` - the option documented here
381+
382+
??? example "Examples"
383+
384+
**Input Schema:**
385+
386+
```json
387+
{
388+
"$schema": "http://json-schema.org/draft-07/schema",
389+
"type": "object",
390+
"properties": {"s": {"type": ["string"]}},
391+
"required": ["s"]
392+
}
393+
```
394+
395+
**Output:**
396+
397+
> **Error:** File not found: jsonschema/simple_string.py
398+
399+
---
400+
329401
## `--url` {#url}
330402

331403
Fetch schema from URL with custom HTTP headers.

docs/cli-reference/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This documentation is auto-generated from test cases.
88

99
| Category | Options | Description |
1010
|----------|---------|-------------|
11-
| 📁 [Base Options](base-options.md) | 7 | Input/output configuration |
11+
| 📁 [Base Options](base-options.md) | 9 | Input/output configuration |
1212
| 🔧 [Typing Customization](typing-customization.md) | 27 | Type annotation and import behavior |
1313
| 🏷️ [Field Customization](field-customization.md) | 24 | Field naming and docstring behavior |
1414
| 🏗️ [Model Customization](model-customization.md) | 39 | Model generation behavior |
@@ -161,6 +161,8 @@ This documentation is auto-generated from test cases.
161161

162162
### S {#s}
163163

164+
- [`--schema-version`](base-options.md#schema-version)
165+
- [`--schema-version-mode`](base-options.md#schema-version-mode)
164166
- [`--set-default-enum-member`](field-customization.md#set-default-enum-member)
165167
- [`--shared-module-name`](general-options.md#shared-module-name)
166168
- [`--skip-root-model`](model-customization.md#skip-root-model)

docs/cli-reference/quick-reference.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ datamodel-codegen [OPTIONS]
2222
| [`--input-model`](base-options.md#input-model) | Import a Python type or dict schema from a module. |
2323
| [`--input-model-ref-strategy`](base-options.md#input-model-ref-strategy) | Strategy for referenced types when using --input-model. |
2424
| [`--output`](base-options.md#output) | Specify the destination path for generated Python code. |
25+
| [`--schema-version`](base-options.md#schema-version) | Schema version to use for parsing. |
26+
| [`--schema-version-mode`](base-options.md#schema-version-mode) | Schema version validation mode. |
2527
| [`--url`](base-options.md#url) | Fetch schema from URL with custom HTTP headers. |
2628

2729
### 🔧 Typing Customization
@@ -299,6 +301,8 @@ All options sorted alphabetically:
299301
- [`--remove-special-field-name-prefix`](field-customization.md#remove-special-field-name-prefix) - Remove the special prefix from field names.
300302
- [`--reuse-model`](model-customization.md#reuse-model) - Reuse identical model definitions instead of generating dupl...
301303
- [`--reuse-scope`](model-customization.md#reuse-scope) - Scope for model reuse detection (root or tree).
304+
- [`--schema-version`](base-options.md#schema-version) - Schema version to use for parsing.
305+
- [`--schema-version-mode`](base-options.md#schema-version-mode) - Schema version validation mode.
302306
- [`--set-default-enum-member`](field-customization.md#set-default-enum-member) - Set the first enum member as the default value for enum fiel...
303307
- [`--shared-module-name`](general-options.md#shared-module-name) - Customize the name of the shared module for deduplicated mod...
304308
- [`--skip-root-model`](model-customization.md#skip-root-model) - Skip generation of root model when schema contains nested de...

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/_types/parser_config_dicts.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919
CollapseRootModelsNameStrategy,
2020
DataclassArguments,
2121
FieldTypeCollisionStrategy,
22+
JsonSchemaVersion,
2223
NamingStrategy,
2324
OpenAPIScope,
25+
OpenAPIVersion,
2426
ReadOnlyWriteOnlyModelType,
2527
ReuseScope,
2628
StrictTypes,
2729
TargetPydanticVersion,
30+
VersionMode,
2831
)
2932
from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion
3033
from datamodel_code_generator.model.base import DataModel, DataModelFieldBase
@@ -167,14 +170,16 @@ class GraphQLParserConfigDict(ParserConfigDict, closed=True):
167170

168171

169172
class JSONSchemaParserConfigDict(ParserConfigDict):
170-
pass
173+
jsonschema_version: NotRequired[JsonSchemaVersion | None]
174+
schema_version_mode: NotRequired[VersionMode | None]
171175

172176

173177
class OpenAPIParserConfigDict(JSONSchemaParserConfigDict, closed=True):
174178
openapi_scopes: NotRequired[list[OpenAPIScope] | None]
175179
include_path_parameters: NotRequired[bool]
176180
use_status_code_in_response_name: NotRequired[bool]
177181
openapi_include_paths: NotRequired[list[str] | None]
182+
openapi_version: NotRequired[OpenAPIVersion | None]
178183

179184

180185
ModelDict: TypeAlias = ParserConfigDict | GraphQLParserConfigDict | JSONSchemaParserConfigDict | OpenAPIParserConfigDict

src/datamodel_code_generator/arguments.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
StrictTypes,
3535
TargetPydanticVersion,
3636
UnionMode,
37+
VersionMode,
3738
)
3839
from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion
3940
from datamodel_code_generator.parser import LiteralType
@@ -984,6 +985,25 @@ def start_section(self, heading: str | None) -> None:
984985
action="store_true",
985986
default=None,
986987
)
988+
# ======================================================================================
989+
# Schema version options (for both JSON Schema and OpenAPI)
990+
# ======================================================================================
991+
base_options.add_argument(
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],
1005+
default=None,
1006+
)
9871007

9881008
# ======================================================================================
9891009
# Options specific to GraphQL input schemas

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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@
2222
FieldTypeCollisionStrategy,
2323
GraphQLScope,
2424
InputFileType,
25+
JsonSchemaVersion,
2526
ModuleSplitMode,
2627
NamingStrategy,
2728
OpenAPIScope,
29+
OpenAPIVersion,
2830
ReadOnlyWriteOnlyModelType,
2931
ReuseScope,
3032
TargetPydanticVersion,
33+
VersionMode,
3134
)
3235
from datamodel_code_generator.format import (
3336
DateClassType,
@@ -205,6 +208,8 @@ class Config:
205208
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None
206209
module_split_mode: ModuleSplitMode | None = None
207210
default_value_overrides: Mapping[str, Any] | None = None
211+
schema_version: str | None = None
212+
schema_version_mode: VersionMode | None = None
208213

209214

210215
class ParserConfig(BaseModel):
@@ -350,6 +355,9 @@ class GraphQLParserConfig(ParserConfig):
350355
class JSONSchemaParserConfig(ParserConfig):
351356
"""Configuration model for JsonSchemaParser.__init__()."""
352357

358+
jsonschema_version: JsonSchemaVersion | None = None
359+
schema_version_mode: VersionMode | None = None
360+
353361

354362
class OpenAPIParserConfig(JSONSchemaParserConfig):
355363
"""Configuration model for OpenAPIParser.__init__()."""
@@ -358,6 +366,7 @@ class OpenAPIParserConfig(JSONSchemaParserConfig):
358366
include_path_parameters: bool = False
359367
use_status_code_in_response_name: bool = False
360368
openapi_include_paths: list[str] | None = None
369+
openapi_version: OpenAPIVersion | None = None
361370

362371

363372
class ParseConfig(BaseModel):

0 commit comments

Comments
 (0)