Skip to content

Commit 7b83709

Browse files
Add inferred union variant names (#3442)
* Add inferred union variant names * Fix union variant name CI checks * Update prompt expected output * Cover union variant name helpers * style: auto-fix by pre-commit.ci * Address union variant name review * style: auto-fix by pre-commit.ci * Cover union variant name fallback * Satisfy CodeQL for variant helpers * Keep union naming off hot path * Avoid duplicate inferred variant names * Clarify inferred union variant example --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e224311 commit 7b83709

30 files changed

Lines changed: 637 additions & 9 deletions

docs/cli-reference/field-customization.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
| [`--field-extra-keys-without-x-prefix`](#field-extra-keys-without-x-prefix) | Include schema extension keys in Field() without requiring '... |
1515
| [`--field-include-all-keys`](#field-include-all-keys) | Include all schema keys in Field() json_schema_extra. |
1616
| [`--field-type-collision-strategy`](#field-type-collision-strategy) | Rename type class instead of field when names collide (Pydan... |
17+
| [`--infer-union-variant-names`](#infer-union-variant-names) | Infer names for inline oneOf/anyOf object variants from lite... |
1718
| [`--no-alias`](#no-alias) | Disable Field alias generation for non-Python-safe property ... |
1819
| [`--original-field-name-delimiter`](#original-field-name-delimiter) | Specify delimiter for original field names when using snake-... |
1920
| [`--remove-special-field-name-prefix`](#remove-special-field-name-prefix) | Remove the special prefix from field names. |
@@ -1742,6 +1743,123 @@ to preserve the original field name, instead of renaming the field and adding an
17421743

17431744
---
17441745

1746+
## `--infer-union-variant-names` {#infer-union-variant-names}
1747+
1748+
Infer names for inline oneOf/anyOf object variants from literal fields.
1749+
1750+
The `--infer-union-variant-names` flag uses branch-local `const` values or
1751+
single-value enums on a shared discriminator-style property to name generated
1752+
inline union models. This is useful when inline union branches would otherwise
1753+
receive position-based names such as `Event` and `Event1`.
1754+
1755+
The literal value can also come from a single-value enum or an internal `$ref`.
1756+
Existing generated output is preserved unless this option is enabled.
1757+
1758+
**Related:** [`--use-title-as-name`](field-customization.md#use-title-as-name)
1759+
1760+
!!! tip "Usage"
1761+
1762+
```bash
1763+
datamodel-codegen --input schema.json --infer-union-variant-names # (1)!
1764+
```
1765+
1766+
1. :material-arrow-left: `--infer-union-variant-names` - the option documented here
1767+
1768+
??? example "Examples"
1769+
1770+
**Input Schema:**
1771+
1772+
```json
1773+
{
1774+
"$schema": "http://json-schema.org/draft-07/schema#",
1775+
"title": "WebhookEnvelope",
1776+
"type": "object",
1777+
"required": ["event"],
1778+
"properties": {
1779+
"event": {
1780+
"oneOf": [
1781+
{
1782+
"type": "object",
1783+
"required": ["type", "id"],
1784+
"properties": {
1785+
"type": {"const": "message.created"},
1786+
"id": {"type": "string"}
1787+
}
1788+
},
1789+
{
1790+
"type": "object",
1791+
"required": ["type", "reason"],
1792+
"properties": {
1793+
"type": {"const": "message.failed"},
1794+
"reason": {"type": "string"}
1795+
}
1796+
}
1797+
]
1798+
}
1799+
}
1800+
}
1801+
```
1802+
1803+
**Output:**
1804+
1805+
=== "With Option"
1806+
1807+
```python
1808+
# generated by datamodel-codegen:
1809+
# filename: infer_union_variant_names.json
1810+
# timestamp: 2019-07-26T00:00:00+00:00
1811+
1812+
from __future__ import annotations
1813+
1814+
from typing import Literal
1815+
1816+
from pydantic import BaseModel
1817+
1818+
1819+
class EventMessageCreated(BaseModel):
1820+
type: Literal['message.created']
1821+
id: str
1822+
1823+
1824+
class EventMessageFailed(BaseModel):
1825+
type: Literal['message.failed']
1826+
reason: str
1827+
1828+
1829+
class WebhookEnvelope(BaseModel):
1830+
event: EventMessageCreated | EventMessageFailed
1831+
```
1832+
1833+
=== "Without Option"
1834+
1835+
```python
1836+
# generated by datamodel-codegen:
1837+
# filename: infer_union_variant_names.json
1838+
# timestamp: 2019-07-26T00:00:00+00:00
1839+
1840+
from __future__ import annotations
1841+
1842+
from typing import Literal
1843+
1844+
from pydantic import BaseModel
1845+
1846+
1847+
class Event(BaseModel):
1848+
type: Literal['message.created']
1849+
id: str
1850+
1851+
1852+
class Event1(BaseModel):
1853+
type: Literal['message.failed']
1854+
reason: str
1855+
1856+
1857+
class WebhookEnvelope(BaseModel):
1858+
event: Event | Event1
1859+
```
1860+
1861+
---
1862+
17451863
## `--no-alias` {#no-alias}
17461864

17471865
Disable Field alias generation for non-Python-safe property names.

docs/cli-reference/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This documentation is auto-generated from test cases.
1010
|----------|---------|-------------|
1111
| 📁 [Base Options](base-options.md) | 11 | Input/output configuration |
1212
| 🔧 [Typing Customization](typing-customization.md) | 30 | Type annotation and import behavior |
13-
| 🏷️ [Field Customization](field-customization.md) | 26 | Field naming and docstring behavior |
13+
| 🏷️ [Field Customization](field-customization.md) | 27 | Field naming and docstring behavior |
1414
| 🏗️ [Model Customization](model-customization.md) | 41 | Model generation behavior |
1515
| 🎨 [Template Customization](template-customization.md) | 22 | Output formatting and custom rendering |
1616
| 📘 [OpenAPI-only Options](openapi-only-options.md) | 8 | OpenAPI-specific features |
@@ -118,6 +118,7 @@ This documentation is auto-generated from test cases.
118118
- [`--ignore-enum-constraints`](typing-customization.md#ignore-enum-constraints)
119119
- [`--ignore-pyproject`](general-options.md#ignore-pyproject)
120120
- [`--include-path-parameters`](openapi-only-options.md#include-path-parameters)
121+
- [`--infer-union-variant-names`](field-customization.md#infer-union-variant-names)
121122
- [`--input`](base-options.md#input)
122123
- [`--input-file-type`](base-options.md#input-file-type)
123124
- [`--input-model`](base-options.md#input-model)

docs/cli-reference/quick-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ datamodel-codegen [OPTIONS]
7777
| [`--field-extra-keys-without-x-prefix`](field-customization.md#field-extra-keys-without-x-prefix) | Include schema extension keys in Field() without requiring 'x-' prefix. |
7878
| [`--field-include-all-keys`](field-customization.md#field-include-all-keys) | Include all schema keys in Field() json_schema_extra. |
7979
| [`--field-type-collision-strategy`](field-customization.md#field-type-collision-strategy) | Rename type class instead of field when names collide (Pydantic v2 only). |
80+
| [`--infer-union-variant-names`](field-customization.md#infer-union-variant-names) | Infer names for inline oneOf/anyOf object variants from literal fields. |
8081
| [`--no-alias`](field-customization.md#no-alias) | Disable Field alias generation for non-Python-safe property names. |
8182
| [`--original-field-name-delimiter`](field-customization.md#original-field-name-delimiter) | Specify delimiter for original field names when using snake-case conversion. |
8283
| [`--remove-special-field-name-prefix`](field-customization.md#remove-special-field-name-prefix) | Remove the special prefix from field names. |
@@ -299,6 +300,7 @@ All options sorted alphabetically:
299300
- [`--ignore-enum-constraints`](typing-customization.md#ignore-enum-constraints) - Ignore enum constraints and use base string type instead of ...
300301
- [`--ignore-pyproject`](general-options.md#ignore-pyproject) - Ignore pyproject.toml configuration file.
301302
- [`--include-path-parameters`](openapi-only-options.md#include-path-parameters) - Include OpenAPI path parameters in generated parameter model...
303+
- [`--infer-union-variant-names`](field-customization.md#infer-union-variant-names) - Infer names for inline oneOf/anyOf object variants from lite...
302304
- [`--input`](base-options.md#input) - Specify the input schema file path.
303305
- [`--input-file-type`](base-options.md#input-file-type) - Specify the input file type for code generation.
304306
- [`--input-model`](base-options.md#input-model) - Import a Python type or dict schema from a module.

skills/datamodel-code-generator/references/cli-options.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Field naming, aliases, defaults, and constraints.
6969
- `--extra-fields`: Set the generated models to allow, forbid, or ignore extra fields. Choices: `allow`, `ignore`, `forbid`.
7070
- `--use-schema-description`: Use schema description to populate class docstring
7171
- `--use-title-as-name`: use titles as class names of models
72+
- `--infer-union-variant-names`: Infer inline oneOf/anyOf branch model names from literal discriminator-style fields
7273
- `--field-constraints`: Use field constraints and not con* annotations
7374
- `--set-default-enum-member`: Set enum members as default values for enum field
7475
- `--use-enum-values-in-discriminator`: Use enum member literals in discriminator fields instead of string literals

src/datamodel_code_generator/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
11131113
graphql_no_typename=config.graphql_no_typename,
11141114
wrap_string_literal=config.wrap_string_literal,
11151115
use_title_as_name=config.use_title_as_name,
1116+
infer_union_variant_names=config.infer_union_variant_names,
11161117
use_operation_id_as_name=config.use_operation_id_as_name,
11171118
use_unique_items_as_set=config.use_unique_items_as_set,
11181119
use_tuple_for_fixed_items=config.use_tuple_for_fixed_items,

src/datamodel_code_generator/_types/generate_config_dict.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class BaseGenerateConfig(TypedDict):
114114
graphql_no_typename: NotRequired[bool]
115115
wrap_string_literal: NotRequired[bool | None]
116116
use_title_as_name: NotRequired[bool]
117+
infer_union_variant_names: NotRequired[bool]
117118
use_operation_id_as_name: NotRequired[bool]
118119
use_unique_items_as_set: NotRequired[bool]
119120
use_tuple_for_fixed_items: NotRequired[bool]

src/datamodel_code_generator/_types/parser_config_dicts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class ParserConfigDict(TypedDict):
113113
model_extra_keys_without_x_prefix: NotRequired[set[str] | None]
114114
wrap_string_literal: NotRequired[bool | None]
115115
use_title_as_name: NotRequired[bool]
116+
infer_union_variant_names: NotRequired[bool]
116117
use_operation_id_as_name: NotRequired[bool]
117118
use_unique_items_as_set: NotRequired[bool]
118119
use_tuple_for_fixed_items: NotRequired[bool]

src/datamodel_code_generator/arguments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,12 @@ def start_section(self, heading: str | None) -> None:
492492
action="store_true",
493493
default=None,
494494
)
495+
model_options.add_argument(
496+
"--infer-union-variant-names",
497+
help="Infer inline oneOf/anyOf branch model names from literal discriminator-style fields",
498+
action="store_true",
499+
default=None,
500+
)
495501
model_options.add_argument(
496502
"--use-pendulum",
497503
help="use pendulum instead of datetime",

src/datamodel_code_generator/cli_options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class CLIOptionMeta:
256256
"--no-alias": CLIOptionMeta(name="--no-alias", category=OptionCategory.FIELD),
257257
"--use-serialization-alias": CLIOptionMeta(name="--use-serialization-alias", category=OptionCategory.FIELD),
258258
"--use-title-as-name": CLIOptionMeta(name="--use-title-as-name", category=OptionCategory.FIELD),
259+
"--infer-union-variant-names": CLIOptionMeta(name="--infer-union-variant-names", category=OptionCategory.FIELD),
259260
"--use-schema-description": CLIOptionMeta(name="--use-schema-description", category=OptionCategory.FIELD),
260261
"--use-field-description": CLIOptionMeta(name="--use-field-description", category=OptionCategory.FIELD),
261262
"--use-field-description-example": CLIOptionMeta(

src/datamodel_code_generator/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class BaseGenerateConfig(BaseModel):
137137
graphql_no_typename: bool = False
138138
wrap_string_literal: bool | None = None
139139
use_title_as_name: bool = False
140+
infer_union_variant_names: bool = False
140141
use_operation_id_as_name: bool = False
141142
use_unique_items_as_set: bool = False
142143
use_tuple_for_fixed_items: bool = False
@@ -299,6 +300,7 @@ class ParserConfig(BaseModel):
299300
model_extra_keys_without_x_prefix: set[str] | None = None
300301
wrap_string_literal: bool | None = None
301302
use_title_as_name: bool = False
303+
infer_union_variant_names: bool = False
302304
use_operation_id_as_name: bool = False
303305
use_unique_items_as_set: bool = False
304306
use_tuple_for_fixed_items: bool = False

0 commit comments

Comments
 (0)