Skip to content

Commit 60f7335

Browse files
authored
Add schema version detection and feature flags (#2924)
* Add schema version detection and feature flags * Refactor to use pattern matching and function-style tests * Use TypeAlias for version patterns dict * Simplify heuristic to default to Draft 2020-12 * Add format registry with separation of OpenAPI-specific formats (#2927) * Add format registry with separation of OpenAPI-specific formats * Use snapshot for full format comparison in tests * Integrate format registry with parsers (backward compatible)
1 parent d1d6f4f commit 60f7335

5 files changed

Lines changed: 764 additions & 10 deletions

File tree

src/datamodel_code_generator/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,15 @@
4343
GraphQLScope,
4444
InputFileType,
4545
InputModelRefStrategy,
46+
JsonSchemaVersion,
4647
ModuleSplitMode,
4748
NamingStrategy,
4849
OpenAPIScope,
50+
OpenAPIVersion,
4951
ReadOnlyWriteOnlyModelType,
5052
ReuseScope,
5153
TargetPydanticVersion,
54+
VersionMode,
5255
)
5356
from datamodel_code_generator.format import (
5457
DEFAULT_FORMATTERS,
@@ -930,6 +933,8 @@ def infer_input_type(text: str) -> InputFileType:
930933

931934
_LAZY_IMPORTS = {
932935
"clear_dynamic_models_cache": "datamodel_code_generator.dynamic",
936+
"detect_jsonschema_version": "datamodel_code_generator.parser.schema_version",
937+
"detect_openapi_version": "datamodel_code_generator.parser.schema_version",
933938
"generate_dynamic_models": "datamodel_code_generator.dynamic",
934939
}
935940

@@ -966,17 +971,22 @@ def __getattr__(name: str) -> Any:
966971
"InputModelRefStrategy",
967972
"InvalidClassNameError",
968973
"InvalidFileFormatError",
974+
"JsonSchemaVersion",
969975
"LiteralType",
970976
"ModuleSplitMode",
971977
"NamingStrategy",
972978
"OpenAPIScope",
979+
"OpenAPIVersion",
973980
"PythonVersion",
974981
"PythonVersionMin",
975982
"ReadOnlyWriteOnlyModelType",
976983
"ReuseScope",
977984
"SchemaParseError",
978985
"TargetPydanticVersion",
986+
"VersionMode",
979987
"clear_dynamic_models_cache", # noqa: F822
988+
"detect_jsonschema_version", # noqa: F822
989+
"detect_openapi_version", # noqa: F822
980990
"generate",
981991
"generate_dynamic_models", # noqa: F822
982992
]

src/datamodel_code_generator/enums.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,42 @@ class StrictTypes(Enum):
240240
bool = "bool"
241241

242242

243+
class JsonSchemaVersion(Enum):
244+
"""JSON Schema draft versions.
245+
246+
Auto: Auto-detect from $schema field or heuristics (default).
247+
"""
248+
249+
Draft4 = "draft-04"
250+
Draft6 = "draft-06"
251+
Draft7 = "draft-07"
252+
Draft201909 = "2019-09"
253+
Draft202012 = "2020-12"
254+
Auto = "auto"
255+
256+
257+
class OpenAPIVersion(Enum):
258+
"""OpenAPI specification versions.
259+
260+
Auto: Auto-detect from openapi field (default).
261+
"""
262+
263+
V30 = "3.0"
264+
V31 = "3.1"
265+
Auto = "auto"
266+
267+
268+
class VersionMode(Enum):
269+
"""Schema version validation mode.
270+
271+
Lenient: Accept all features regardless of declared version (default).
272+
Strict: Warn on features outside declared/detected version.
273+
"""
274+
275+
Lenient = "lenient"
276+
Strict = "strict"
277+
278+
243279
__all__ = [
244280
"DEFAULT_SHARED_MODULE_NAME",
245281
"MAX_VERSION",
@@ -256,12 +292,15 @@ class StrictTypes(Enum):
256292
"GraphQLScope",
257293
"InputFileType",
258294
"InputModelRefStrategy",
295+
"JsonSchemaVersion",
259296
"ModuleSplitMode",
260297
"NamingStrategy",
261298
"OpenAPIScope",
299+
"OpenAPIVersion",
262300
"ReadOnlyWriteOnlyModelType",
263301
"ReuseScope",
264302
"StrictTypes",
265303
"TargetPydanticVersion",
266304
"UnionMode",
305+
"VersionMode",
267306
]

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -509,15 +509,21 @@ def get_ref_type(ref: str) -> JSONReference:
509509
return JSONReference.REMOTE
510510

511511

512-
def _get_type(type_: str, format__: str | None = None) -> Types:
512+
def _get_type(
513+
type_: str,
514+
format__: str | None = None,
515+
data_formats: dict[str, dict[str, Types]] | None = None,
516+
) -> Types:
513517
"""Get the appropriate Types enum for a given JSON Schema type and format."""
514-
if type_ not in json_schema_data_formats:
518+
if data_formats is None:
519+
data_formats = json_schema_data_formats
520+
if type_ not in data_formats:
515521
return Types.any
516-
if (data_formats := json_schema_data_formats[type_].get("default" if format__ is None else format__)) is not None:
517-
return data_formats
522+
if (type_format := data_formats[type_].get("default" if format__ is None else format__)) is not None:
523+
return type_format
518524

519525
warn(f"format of {format__!r} not understood for {type_!r} - using default", stacklevel=2)
520-
return json_schema_data_formats[type_]["default"]
526+
return data_formats[type_]["default"]
521527

522528

523529
JsonSchemaObject.model_rebuild()
@@ -732,21 +738,31 @@ def get_field_extras(self, obj: JsonSchemaObject) -> dict[str, Any]:
732738
extras.update(self.default_field_extras)
733739
return extras
734740

741+
@cached_property
742+
def _data_formats(self) -> dict[str, dict[str, Types]]:
743+
"""Get data format mappings for this parser type.
744+
745+
Returns all formats for backward compatibility.
746+
OpenAPI-specific formats will be separated in Strict mode (future).
747+
"""
748+
return json_schema_data_formats
749+
735750
def _get_type_with_mappings(self, type_: str, format_: str | None = None) -> Types:
736751
"""Get the Types enum for a given type and format, applying custom type mappings.
737752
738753
Custom mappings from --type-mappings are checked first, then falls back to
739-
the default json_schema_data_formats mappings.
754+
the parser's data format mappings.
740755
"""
756+
data_formats = self._data_formats
741757
if self.type_mappings and format_ is not None and (type_, format_) in self.type_mappings:
742758
target_format = self.type_mappings[type_, format_]
743-
for type_formats in json_schema_data_formats.values():
759+
for type_formats in data_formats.values():
744760
if target_format in type_formats:
745761
return type_formats[target_format]
746-
if target_format in json_schema_data_formats:
747-
return json_schema_data_formats[target_format]["default"]
762+
if target_format in data_formats:
763+
return data_formats[target_format]["default"]
748764

749-
return _get_type(type_, format_)
765+
return _get_type(type_, format_, data_formats)
750766

751767
@cached_property
752768
def schema_paths(self) -> list[tuple[str, list[str]]]:

0 commit comments

Comments
 (0)