Skip to content

Commit b1d78c6

Browse files
committed
Add schema version detection and feature flags
1 parent 58e73ed commit b1d78c6

4 files changed

Lines changed: 477 additions & 0 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
]
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Schema version features and detection utilities.
2+
3+
Provides SchemaFeatures classes for version-dependent feature flags
4+
and utility functions for detecting schema versions.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from dataclasses import dataclass
10+
from typing import Any, TypeVar
11+
12+
from datamodel_code_generator.enums import JsonSchemaVersion, OpenAPIVersion
13+
14+
15+
@dataclass(frozen=True)
16+
class JsonSchemaFeatures:
17+
"""Feature flags for JSON Schema versions.
18+
19+
This is the base class for schema features. OpenAPISchemaFeatures
20+
extends this to add OpenAPI-specific features.
21+
22+
Attributes:
23+
null_in_type_array: Draft 2020-12 allows null in type arrays.
24+
defs_not_definitions: Draft 2019-09+ uses $defs instead of definitions.
25+
prefix_items: Draft 2020-12 uses prefixItems instead of items array.
26+
boolean_schemas: Draft 6+ allows boolean values as schemas.
27+
id_field: The field name for schema ID ("id" for Draft 4, "$id" for Draft 6+).
28+
definitions_key: The key for definitions ("definitions" or "$defs").
29+
"""
30+
31+
null_in_type_array: bool
32+
defs_not_definitions: bool
33+
prefix_items: bool
34+
boolean_schemas: bool
35+
id_field: str
36+
definitions_key: str
37+
38+
@classmethod
39+
def from_version(cls, version: JsonSchemaVersion) -> JsonSchemaFeatures:
40+
"""Create JsonSchemaFeatures from a JSON Schema version."""
41+
if version == JsonSchemaVersion.Draft4:
42+
return cls(
43+
null_in_type_array=False,
44+
defs_not_definitions=False,
45+
prefix_items=False,
46+
boolean_schemas=False,
47+
id_field="id",
48+
definitions_key="definitions",
49+
)
50+
if version in {JsonSchemaVersion.Draft6, JsonSchemaVersion.Draft7}:
51+
return cls(
52+
null_in_type_array=False,
53+
defs_not_definitions=False,
54+
prefix_items=False,
55+
boolean_schemas=True,
56+
id_field="$id",
57+
definitions_key="definitions",
58+
)
59+
if version == JsonSchemaVersion.Draft201909:
60+
return cls(
61+
null_in_type_array=False,
62+
defs_not_definitions=True,
63+
prefix_items=False,
64+
boolean_schemas=True,
65+
id_field="$id",
66+
definitions_key="$defs",
67+
)
68+
# Draft 2020-12 or Auto (default to latest features in lenient mode)
69+
return cls(
70+
null_in_type_array=True,
71+
defs_not_definitions=True,
72+
prefix_items=True,
73+
boolean_schemas=True,
74+
id_field="$id",
75+
definitions_key="$defs",
76+
)
77+
78+
79+
@dataclass(frozen=True)
80+
class OpenAPISchemaFeatures(JsonSchemaFeatures):
81+
"""Feature flags for OpenAPI versions.
82+
83+
Extends JsonSchemaFeatures with OpenAPI-specific features.
84+
85+
Attributes:
86+
nullable_keyword: OpenAPI 3.0 uses nullable: true (deprecated in 3.1).
87+
discriminator_support: All OpenAPI versions support discriminator.
88+
"""
89+
90+
nullable_keyword: bool
91+
discriminator_support: bool
92+
93+
@classmethod
94+
def from_openapi_version(cls, version: OpenAPIVersion) -> OpenAPISchemaFeatures:
95+
"""Create OpenAPISchemaFeatures from an OpenAPI version."""
96+
if version == OpenAPIVersion.V30:
97+
# OpenAPI 3.0 does not support boolean schemas (only 3.1+ does)
98+
return cls(
99+
null_in_type_array=False,
100+
defs_not_definitions=False,
101+
prefix_items=False,
102+
boolean_schemas=False,
103+
id_field="$id",
104+
definitions_key="definitions",
105+
nullable_keyword=True,
106+
discriminator_support=True,
107+
)
108+
# OpenAPI 3.1 or Auto (default to latest features in lenient mode)
109+
return cls(
110+
null_in_type_array=True,
111+
defs_not_definitions=True,
112+
prefix_items=True,
113+
boolean_schemas=True,
114+
id_field="$id",
115+
definitions_key="$defs",
116+
nullable_keyword=False,
117+
discriminator_support=True,
118+
)
119+
120+
121+
# Type variable for SchemaFeatures subclasses
122+
SchemaFeaturesT = TypeVar("SchemaFeaturesT", bound=JsonSchemaFeatures)
123+
124+
125+
def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: # noqa: PLR0911
126+
"""Detect JSON Schema version from $schema field or heuristics.
127+
128+
Detection priority:
129+
1. $schema field explicit declaration
130+
2. Heuristics ($defs vs definitions, etc.)
131+
3. Fallback: Draft7 (most widely used)
132+
133+
Note: In Lenient mode, detection result is only used for optimization hints.
134+
In Strict mode, detection result is used to warn on version violations.
135+
136+
Args:
137+
data: The schema dictionary to analyze.
138+
139+
Returns:
140+
The detected JSON Schema version.
141+
"""
142+
schema_url = data.get("$schema", "")
143+
if isinstance(schema_url, str):
144+
if "draft-04" in schema_url:
145+
return JsonSchemaVersion.Draft4
146+
if "draft-06" in schema_url:
147+
return JsonSchemaVersion.Draft6
148+
if "draft-07" in schema_url:
149+
return JsonSchemaVersion.Draft7
150+
if "2019-09" in schema_url:
151+
return JsonSchemaVersion.Draft201909
152+
if "2020-12" in schema_url:
153+
return JsonSchemaVersion.Draft202012
154+
155+
# Heuristic detection (when $schema is missing)
156+
if "$defs" in data:
157+
# $defs was introduced in Draft 2019-09
158+
# prefixItems is specific to Draft 2020-12
159+
if "prefixItems" in data:
160+
return JsonSchemaVersion.Draft202012
161+
return JsonSchemaVersion.Draft201909
162+
if "definitions" in data:
163+
return JsonSchemaVersion.Draft7
164+
165+
# Fallback: Draft7 is the most widely used
166+
return JsonSchemaVersion.Draft7
167+
168+
169+
def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion:
170+
"""Detect OpenAPI version from openapi field.
171+
172+
Args:
173+
data: The schema dictionary to analyze.
174+
175+
Returns:
176+
The detected OpenAPI version.
177+
"""
178+
version = data.get("openapi", "")
179+
if isinstance(version, str):
180+
if version.startswith("3.1"):
181+
return OpenAPIVersion.V31
182+
if version.startswith("3.0"):
183+
return OpenAPIVersion.V30
184+
# Fallback: 3.1 (latest, best JSON Schema compatibility)
185+
return OpenAPIVersion.V31
186+
187+
188+
__all__ = [
189+
"JsonSchemaFeatures",
190+
"OpenAPISchemaFeatures",
191+
"SchemaFeaturesT",
192+
"detect_jsonschema_version",
193+
"detect_openapi_version",
194+
]

0 commit comments

Comments
 (0)