Skip to content

Commit 846d77c

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

4 files changed

Lines changed: 506 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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,43 @@ 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/swagger field (default).
261+
"""
262+
263+
V2 = "2.0"
264+
V30 = "3.0"
265+
V31 = "3.1"
266+
Auto = "auto"
267+
268+
269+
class VersionMode(Enum):
270+
"""Schema version validation mode.
271+
272+
Lenient: Accept all features regardless of declared version (default).
273+
Strict: Warn on features outside declared/detected version.
274+
"""
275+
276+
Lenient = "lenient"
277+
Strict = "strict"
278+
279+
243280
__all__ = [
244281
"DEFAULT_SHARED_MODULE_NAME",
245282
"MAX_VERSION",
@@ -256,12 +293,15 @@ class StrictTypes(Enum):
256293
"GraphQLScope",
257294
"InputFileType",
258295
"InputModelRefStrategy",
296+
"JsonSchemaVersion",
259297
"ModuleSplitMode",
260298
"NamingStrategy",
261299
"OpenAPIScope",
300+
"OpenAPIVersion",
262301
"ReadOnlyWriteOnlyModelType",
263302
"ReuseScope",
264303
"StrictTypes",
265304
"TargetPydanticVersion",
266305
"UnionMode",
306+
"VersionMode",
267307
]
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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.V2:
97+
# OpenAPI 2.0 (Swagger) uses JSON Schema Draft 4 semantics
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", # Draft 4 uses "id", not "$id"
104+
definitions_key="definitions",
105+
nullable_keyword=False,
106+
discriminator_support=True,
107+
)
108+
if version == OpenAPIVersion.V30:
109+
# OpenAPI 3.0 does not support boolean schemas (only 3.1+ does)
110+
return cls(
111+
null_in_type_array=False,
112+
defs_not_definitions=False,
113+
prefix_items=False,
114+
boolean_schemas=False,
115+
id_field="$id",
116+
definitions_key="definitions",
117+
nullable_keyword=True,
118+
discriminator_support=True,
119+
)
120+
# OpenAPI 3.1 or Auto (default to latest features in lenient mode)
121+
return cls(
122+
null_in_type_array=True,
123+
defs_not_definitions=True,
124+
prefix_items=True,
125+
boolean_schemas=True,
126+
id_field="$id",
127+
definitions_key="$defs",
128+
nullable_keyword=False,
129+
discriminator_support=True,
130+
)
131+
132+
133+
# Type variable for SchemaFeatures subclasses
134+
SchemaFeaturesT = TypeVar("SchemaFeaturesT", bound=JsonSchemaFeatures)
135+
136+
137+
def detect_jsonschema_version(data: dict[str, Any]) -> JsonSchemaVersion: # noqa: PLR0911
138+
"""Detect JSON Schema version from $schema field or heuristics.
139+
140+
Detection priority:
141+
1. $schema field explicit declaration
142+
2. Heuristics ($defs vs definitions, etc.)
143+
3. Fallback: Draft7 (most widely used)
144+
145+
Note: In Lenient mode, detection result is only used for optimization hints.
146+
In Strict mode, detection result is used to warn on version violations.
147+
148+
Args:
149+
data: The schema dictionary to analyze.
150+
151+
Returns:
152+
The detected JSON Schema version.
153+
"""
154+
schema_url = data.get("$schema", "")
155+
if isinstance(schema_url, str):
156+
if "draft-04" in schema_url:
157+
return JsonSchemaVersion.Draft4
158+
if "draft-06" in schema_url:
159+
return JsonSchemaVersion.Draft6
160+
if "draft-07" in schema_url:
161+
return JsonSchemaVersion.Draft7
162+
if "2019-09" in schema_url:
163+
return JsonSchemaVersion.Draft201909
164+
if "2020-12" in schema_url:
165+
return JsonSchemaVersion.Draft202012
166+
167+
# Heuristic detection (when $schema is missing)
168+
if "$defs" in data:
169+
# $defs was introduced in Draft 2019-09
170+
# prefixItems is specific to Draft 2020-12
171+
if "prefixItems" in data:
172+
return JsonSchemaVersion.Draft202012
173+
return JsonSchemaVersion.Draft201909
174+
if "definitions" in data:
175+
return JsonSchemaVersion.Draft7
176+
177+
# Fallback: Draft7 is the most widely used
178+
return JsonSchemaVersion.Draft7
179+
180+
181+
def detect_openapi_version(data: dict[str, Any]) -> OpenAPIVersion:
182+
"""Detect OpenAPI version from openapi/swagger field.
183+
184+
Args:
185+
data: The schema dictionary to analyze.
186+
187+
Returns:
188+
The detected OpenAPI version.
189+
"""
190+
version = data.get("openapi", data.get("swagger", ""))
191+
if isinstance(version, str):
192+
if version.startswith("3.1"):
193+
return OpenAPIVersion.V31
194+
if version.startswith("3.0"):
195+
return OpenAPIVersion.V30
196+
if version.startswith("2"):
197+
return OpenAPIVersion.V2
198+
# Fallback: 3.1 (latest, best JSON Schema compatibility)
199+
return OpenAPIVersion.V31
200+
201+
202+
__all__ = [
203+
"JsonSchemaFeatures",
204+
"OpenAPISchemaFeatures",
205+
"SchemaFeaturesT",
206+
"detect_jsonschema_version",
207+
"detect_openapi_version",
208+
]

0 commit comments

Comments
 (0)