Skip to content

Commit 73bf200

Browse files
feat: add --allof-class-hierarchy option (#2869)
* feat: add --allof-class-hierarchy option ... to control class hierarchy representation for allOf schemas fix pytest and pre-commit hooks for Windows environments Refs #2645 * fix: add missing allof_class_hierarchy param to config.py * fix: docstring for test_main_allof_class_hierarchy * fix: add allof_class_hierarchy parameter to config dictionaries * fix: reorder import statements for AllOfClassHierarchy --------- Co-authored-by: Koudai Aono <koxudaxi@gmail.com>
1 parent 592723c commit 73bf200

20 files changed

Lines changed: 644 additions & 1 deletion

docs/cli-reference/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This documentation is auto-generated from test cases.
99
| Category | Options | Description |
1010
|----------|---------|-------------|
1111
| 📁 [Base Options](base-options.md) | 7 | Input/output configuration |
12-
| 🔧 [Typing Customization](typing-customization.md) | 26 | Type annotation and import behavior |
12+
| 🔧 [Typing Customization](typing-customization.md) | 27 | Type annotation and import behavior |
1313
| 🏷️ [Field Customization](field-customization.md) | 22 | Field naming and docstring behavior |
1414
| 🏗️ [Model Customization](model-customization.md) | 36 | Model generation behavior |
1515
| 🎨 [Template Customization](template-customization.md) | 18 | Output formatting and custom rendering |
@@ -28,6 +28,7 @@ This documentation is auto-generated from test cases.
2828
- [`--aliases`](field-customization.md#aliases)
2929
- [`--all-exports-collision-strategy`](general-options.md#all-exports-collision-strategy)
3030
- [`--all-exports-scope`](general-options.md#all-exports-scope)
31+
- [`--allof-class-hierarchy`](typing-customization.md#allof-class-hierarchy)
3132
- [`--allof-merge-mode`](typing-customization.md#allof-merge-mode)
3233
- [`--allow-extra-fields`](model-customization.md#allow-extra-fields)
3334
- [`--allow-population-by-field-name`](model-customization.md#allow-population-by-field-name)

docs/cli-reference/quick-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ datamodel-codegen [OPTIONS]
2828

2929
| Option | Description |
3030
|--------|-------------|
31+
| [`--allof-class-hierarchy`](typing-customization.md#allof-class-hierarchy) | Controls how allOf schemas are represented in the generated class hierarchy. |
3132
| [`--allof-merge-mode`](typing-customization.md#allof-merge-mode) | Merge constraints from root model references in allOf schemas. |
3233
| [`--disable-future-imports`](typing-customization.md#disable-future-imports) | Prevent automatic addition of __future__ imports in generated code. |
3334
| [`--enum-field-as-literal`](typing-customization.md#enum-field-as-literal) | Convert all enum fields to Literal types instead of Enum classes. |
@@ -198,6 +199,7 @@ All options sorted alphabetically:
198199
- [`--aliases`](field-customization.md#aliases) - Apply custom field and class name aliases from JSON file.
199200
- [`--all-exports-collision-strategy`](general-options.md#all-exports-collision-strategy) - Handle name collisions when exporting recursive module hiera...
200201
- [`--all-exports-scope`](general-options.md#all-exports-scope) - Generate __all__ exports for child modules in __init__.py fi...
202+
- [`--allof-class-hierarchy`](typing-customization.md#allof-class-hierarchy) - Controls how allOf schemas are represented in the generated ...
201203
- [`--allof-merge-mode`](typing-customization.md#allof-merge-mode) - Merge constraints from root model references in allOf schema...
202204
- [`--allow-extra-fields`](model-customization.md#allow-extra-fields) - Allow extra fields in generated Pydantic models (extra='allo...
203205
- [`--allow-population-by-field-name`](model-customization.md#allow-population-by-field-name) - Allow Pydantic model population by field name (not just alia...

docs/cli-reference/typing-customization.md

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
| Option | Description |
66
|--------|-------------|
7+
| [`--allof-class-hierarchy`](#allof-class-hierarchy) | Controls how allOf schemas are represented in the generated ... |
78
| [`--allof-merge-mode`](#allof-merge-mode) | Merge constraints from root model references in allOf schema... |
89
| [`--disable-future-imports`](#disable-future-imports) | Prevent automatic addition of __future__ imports in generate... |
910
| [`--enum-field-as-literal`](#enum-field-as-literal) | Convert all enum fields to Literal types instead of Enum cla... |
@@ -33,6 +34,333 @@
3334

3435
---
3536

37+
## `--allof-class-hierarchy` {#allof-class-hierarchy}
38+
39+
Controls how allOf schemas are represented in the generated class hierarchy.
40+
`--allof-class-hierarchy if-no-conflict` (default) creates parent classes for allOf schemas
41+
only when there are no property conflicts between parent schemas. Otherwise, properties are merged into the child class
42+
which is then decoupled from the parent classes and no longer inherits from them.
43+
`--allof-class-hierarchy always` keeps class hierarchy for allOf schemas,
44+
even in multiple inheritance scenarios where two parent schemas define the same property.
45+
46+
!!! tip "Usage"
47+
48+
```bash
49+
datamodel-codegen --input schema.json --allof-class-hierarchy always # (1)!
50+
```
51+
52+
1. :material-arrow-left: `--allof-class-hierarchy` - the option documented here
53+
54+
??? example "Examples"
55+
56+
**Input Schema:**
57+
58+
```json
59+
{
60+
"$schema": "http://json-schema.org/draft-07/schema#",
61+
"definitions": {
62+
"StringDatatype": {
63+
"description": "A base string type.",
64+
"type": "string",
65+
"pattern": "^\\S(.*\\S)?$"
66+
},
67+
"ConstrainedStringDatatype": {
68+
"description": "A constrained string.",
69+
"allOf": [
70+
{ "$ref": "#/definitions/StringDatatype" },
71+
{ "type": "string", "minLength": 1, "pattern": "^[A-Z].*" }
72+
]
73+
},
74+
"IntegerDatatype": {
75+
"description": "A whole number.",
76+
"type": "integer"
77+
},
78+
"NonNegativeIntegerDatatype": {
79+
"description": "Non-negative integer.",
80+
"allOf": [
81+
{ "$ref": "#/definitions/IntegerDatatype" },
82+
{ "minimum": 0 }
83+
]
84+
},
85+
"BoundedIntegerDatatype": {
86+
"description": "Integer between 0 and 100.",
87+
"allOf": [
88+
{ "$ref": "#/definitions/IntegerDatatype" },
89+
{ "minimum": 0, "maximum": 100 }
90+
]
91+
},
92+
"EmailDatatype": {
93+
"description": "Email with format.",
94+
"allOf": [
95+
{ "$ref": "#/definitions/StringDatatype" },
96+
{ "format": "email" }
97+
]
98+
},
99+
"FormattedStringDatatype": {
100+
"description": "A string with email format.",
101+
"type": "string",
102+
"format": "email"
103+
},
104+
"ObjectBase": {
105+
"type": "object",
106+
"properties": {
107+
"id": { "type": "integer" }
108+
}
109+
},
110+
"ObjectWithAllOf": {
111+
"description": "Object inheritance - not a root model.",
112+
"allOf": [
113+
{ "$ref": "#/definitions/ObjectBase" },
114+
{ "type": "object", "properties": { "name": { "type": "string" } } }
115+
]
116+
},
117+
"MultiRefAllOf": {
118+
"description": "Multiple refs - not handled by new code.",
119+
"allOf": [
120+
{ "$ref": "#/definitions/StringDatatype" },
121+
{ "$ref": "#/definitions/IntegerDatatype" }
122+
]
123+
},
124+
"NoConstraintAllOf": {
125+
"description": "No constraints added.",
126+
"allOf": [
127+
{ "$ref": "#/definitions/StringDatatype" }
128+
]
129+
},
130+
"IncompatibleTypeAllOf": {
131+
"description": "Incompatible types.",
132+
"allOf": [
133+
{ "$ref": "#/definitions/StringDatatype" },
134+
{ "type": "boolean" }
135+
]
136+
},
137+
"ConstraintWithProperties": {
138+
"description": "Constraint item has properties.",
139+
"allOf": [
140+
{ "$ref": "#/definitions/StringDatatype" },
141+
{ "properties": { "extra": { "type": "string" } } }
142+
]
143+
},
144+
"ConstraintWithItems": {
145+
"description": "Constraint item has items.",
146+
"allOf": [
147+
{ "$ref": "#/definitions/StringDatatype" },
148+
{ "items": { "type": "string" } }
149+
]
150+
},
151+
"NumberIntegerCompatible": {
152+
"description": "Number and integer are compatible.",
153+
"allOf": [
154+
{ "$ref": "#/definitions/IntegerDatatype" },
155+
{ "type": "number", "minimum": 0 }
156+
]
157+
},
158+
"RefWithSchemaKeywords": {
159+
"description": "Ref with additional schema keywords.",
160+
"allOf": [
161+
{ "$ref": "#/definitions/StringDatatype", "minLength": 5 },
162+
{ "maxLength": 100 }
163+
]
164+
},
165+
"ArrayDatatype": {
166+
"type": "array",
167+
"items": { "type": "string" }
168+
},
169+
"RefToArrayAllOf": {
170+
"description": "Ref to array - not a root model.",
171+
"allOf": [
172+
{ "$ref": "#/definitions/ArrayDatatype" },
173+
{ "minItems": 1 }
174+
]
175+
},
176+
"ObjectNoPropsDatatype": {
177+
"type": "object"
178+
},
179+
"RefToObjectNoPropsAllOf": {
180+
"description": "Ref to object without properties - not a root model.",
181+
"allOf": [
182+
{ "$ref": "#/definitions/ObjectNoPropsDatatype" },
183+
{ "minProperties": 1 }
184+
]
185+
},
186+
"PatternPropsDatatype": {
187+
"patternProperties": {
188+
"^S_": { "type": "string" }
189+
}
190+
},
191+
"RefToPatternPropsAllOf": {
192+
"description": "Ref to patternProperties - not a root model.",
193+
"allOf": [
194+
{ "$ref": "#/definitions/PatternPropsDatatype" },
195+
{ "minProperties": 1 }
196+
]
197+
},
198+
"NestedAllOfDatatype": {
199+
"allOf": [
200+
{ "type": "string" },
201+
{ "minLength": 1 }
202+
]
203+
},
204+
"RefToNestedAllOfAllOf": {
205+
"description": "Ref to nested allOf - not a root model.",
206+
"allOf": [
207+
{ "$ref": "#/definitions/NestedAllOfDatatype" },
208+
{ "maxLength": 100 }
209+
]
210+
},
211+
"ConstraintsOnlyDatatype": {
212+
"description": "Constraints only, no type.",
213+
"minLength": 1,
214+
"pattern": "^[A-Z]"
215+
},
216+
"RefToConstraintsOnlyAllOf": {
217+
"description": "Ref to constraints-only schema.",
218+
"allOf": [
219+
{ "$ref": "#/definitions/ConstraintsOnlyDatatype" },
220+
{ "maxLength": 100 }
221+
]
222+
},
223+
"NoDescriptionAllOf": {
224+
"allOf": [
225+
{ "$ref": "#/definitions/StringDatatype" },
226+
{ "minLength": 5 }
227+
]
228+
},
229+
"EmptyConstraintItemAllOf": {
230+
"description": "AllOf with empty constraint item.",
231+
"allOf": [
232+
{ "$ref": "#/definitions/StringDatatype" },
233+
{},
234+
{ "maxLength": 50 }
235+
]
236+
},
237+
"ConflictingFormatAllOf": {
238+
"description": "Conflicting formats - falls back to existing behavior.",
239+
"allOf": [
240+
{ "$ref": "#/definitions/FormattedStringDatatype" },
241+
{ "format": "date-time" }
242+
]
243+
}
244+
},
245+
"type": "object",
246+
"properties": {
247+
"name": { "$ref": "#/definitions/ConstrainedStringDatatype" },
248+
"count": { "$ref": "#/definitions/NonNegativeIntegerDatatype" },
249+
"percentage": { "$ref": "#/definitions/BoundedIntegerDatatype" },
250+
"email": { "$ref": "#/definitions/EmailDatatype" },
251+
"obj": { "$ref": "#/definitions/ObjectWithAllOf" },
252+
"multi": { "$ref": "#/definitions/MultiRefAllOf" },
253+
"noconstraint": { "$ref": "#/definitions/NoConstraintAllOf" },
254+
"incompatible": { "$ref": "#/definitions/IncompatibleTypeAllOf" },
255+
"withprops": { "$ref": "#/definitions/ConstraintWithProperties" },
256+
"withitems": { "$ref": "#/definitions/ConstraintWithItems" },
257+
"numint": { "$ref": "#/definitions/NumberIntegerCompatible" },
258+
"refwithkw": { "$ref": "#/definitions/RefWithSchemaKeywords" },
259+
"refarr": { "$ref": "#/definitions/RefToArrayAllOf" },
260+
"refobjnoprops": { "$ref": "#/definitions/RefToObjectNoPropsAllOf" },
261+
"refpatternprops": { "$ref": "#/definitions/RefToPatternPropsAllOf" },
262+
"refnestedallof": { "$ref": "#/definitions/RefToNestedAllOfAllOf" },
263+
"refconstraintsonly": { "$ref": "#/definitions/RefToConstraintsOnlyAllOf" },
264+
"nodescription": { "$ref": "#/definitions/NoDescriptionAllOf" },
265+
"emptyconstraint": { "$ref": "#/definitions/EmptyConstraintItemAllOf" },
266+
"conflictingformat": { "$ref": "#/definitions/ConflictingFormatAllOf" }
267+
}
268+
}
269+
```
270+
271+
**Output:**
272+
273+
=== "With Option"
274+
275+
```python
276+
# generated by datamodel-codegen:
277+
# filename: allof_class_hierarchy.json
278+
# timestamp: 2019-07-26T00:00:00+00:00
279+
280+
from __future__ import annotations
281+
282+
from pydantic import BaseModel, Field, constr
283+
284+
285+
class Entity(BaseModel):
286+
type: str
287+
type_list: list[str] | None = ['playground:Entity']
288+
289+
290+
class Entity2(BaseModel):
291+
type: str
292+
type_list: list[str]
293+
294+
295+
class Thing(Entity):
296+
type: str
297+
type_list: list[str]
298+
name: constr(min_length=1) = Field(..., description='The things name')
299+
300+
301+
class Location(Entity2):
302+
type: str
303+
type_list: list[str]
304+
address: constr(min_length=5) = Field(
305+
..., description='The address of the location'
306+
)
307+
308+
309+
class Person(Thing, Location):
310+
name: constr(min_length=1) | None = Field(None, description="The person's name")
311+
type: str
312+
type_list: list[str]
313+
```
314+
315+
=== "Without Option"
316+
317+
```python
318+
# generated by datamodel-codegen:
319+
# filename: allof_class_hierarchy.json
320+
# timestamp: 2019-07-26T00:00:00+00:00
321+
322+
from __future__ import annotations
323+
324+
from typing import Any
325+
326+
from pydantic import BaseModel, Field, constr
327+
328+
329+
class Person(BaseModel):
330+
name: constr(min_length=1) = Field(..., description='The things name')
331+
type: Any
332+
type_list: list[Any]
333+
address: constr(min_length=5) = Field(
334+
..., description='The address of the location'
335+
)
336+
337+
338+
class Entity(BaseModel):
339+
type: str
340+
type_list: list[str] | None = ['playground:Entity']
341+
342+
343+
class Entity2(BaseModel):
344+
type: str
345+
type_list: list[str]
346+
347+
348+
class Thing(Entity):
349+
type: str
350+
type_list: list[str]
351+
name: constr(min_length=1) = Field(..., description='The things name')
352+
353+
354+
class Location(Entity2):
355+
type: str
356+
type_list: list[str]
357+
address: constr(min_length=5) = Field(
358+
..., description='The address of the location'
359+
)
360+
```
361+
362+
---
363+
36364
## `--allof-merge-mode` {#allof-merge-mode}
37365

38366
Merge constraints from root model references in allOf schemas.

0 commit comments

Comments
 (0)