Skip to content

Commit 71ddd8b

Browse files
authored
Merge pull request #566 from koxudaxi/issue-480-discriminated-union-simple-types
Fix discriminated union simple types
2 parents 7f653a0 + 6908089 commit 71ddd8b

9 files changed

Lines changed: 339 additions & 6 deletions

File tree

docs/llms-full.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,15 +539,15 @@ Run `tox run -e schema-docs` after changing supported inputs or model backends.
539539

540540
| Format | Status | Evidence | Notes |
541541
|--------|--------|----------|-------|
542-
| OpenAPI YAML | tested | `tests/data/openapi/**/*.yaml` (25 fixtures) | Primary fixture format exercised under `tests/data/openapi/**/*.yaml`. |
542+
| OpenAPI YAML | tested | `tests/data/openapi/**/*.yaml` (26 fixtures) | Primary fixture format exercised under `tests/data/openapi/**/*.yaml`. |
543543
| OpenAPI JSON | tested | `tests/main/test_main.py::test_generate_from_json_input` | Covered by the JSON conversion CLI test in `tests/main/test_main.py`. |
544544
| Remote HTTP `$ref` targets | tested | `tests/main/test_main.py::test_generate_remote_ref` | Covered by the remote `$ref` generation test against a live HTTP server. |
545545

546546
## Fixture Suites
547547

548548
| Suite | Fixtures | Example files | Notes |
549549
|-------|----------|---------------|-------|
550-
| Default template | 17 | `body_and_parameters.yaml`, `content_in_parameters.yaml`, `content_in_parameters_inline.yaml` | Core single-file generation scenarios exercised by the main CLI tests. |
550+
| Default template | 18 | `body_and_parameters.yaml`, `content_in_parameters.yaml`, `content_in_parameters_inline.yaml` | Core single-file generation scenarios exercised by the main CLI tests. |
551551
| Coverage fixtures | 4 | `callbacks.yaml`, `callbacks_with_operation_id.yaml`, `faux_immutability.yaml` | Focused fixtures for callbacks, non-200 responses, and other regression edges. |
552552
| Custom template overrides | 1 | `custom_security.yaml` | Template override coverage for `--template-dir`. |
553553
| Timestamp suppression | 1 | `simple.yaml` | Fixtures that exercise `--disable-timestamp`. |

docs/supported_formats.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ Run `tox run -e schema-docs` after changing supported inputs or model backends.
77

88
| Format | Status | Evidence | Notes |
99
|--------|--------|----------|-------|
10-
| OpenAPI YAML | tested | `tests/data/openapi/**/*.yaml` (25 fixtures) | Primary fixture format exercised under `tests/data/openapi/**/*.yaml`. |
10+
| OpenAPI YAML | tested | `tests/data/openapi/**/*.yaml` (26 fixtures) | Primary fixture format exercised under `tests/data/openapi/**/*.yaml`. |
1111
| OpenAPI JSON | tested | `tests/main/test_main.py::test_generate_from_json_input` | Covered by the JSON conversion CLI test in `tests/main/test_main.py`. |
1212
| Remote HTTP `$ref` targets | tested | `tests/main/test_main.py::test_generate_remote_ref` | Covered by the remote `$ref` generation test against a live HTTP server. |
1313

1414
## Fixture Suites
1515

1616
| Suite | Fixtures | Example files | Notes |
1717
|-------|----------|---------------|-------|
18-
| Default template | 17 | `body_and_parameters.yaml`, `content_in_parameters.yaml`, `content_in_parameters_inline.yaml` | Core single-file generation scenarios exercised by the main CLI tests. |
18+
| Default template | 18 | `body_and_parameters.yaml`, `content_in_parameters.yaml`, `content_in_parameters_inline.yaml` | Core single-file generation scenarios exercised by the main CLI tests. |
1919
| Coverage fixtures | 4 | `callbacks.yaml`, `callbacks_with_operation_id.yaml`, `faux_immutability.yaml` | Focused fixtures for callbacks, non-200 responses, and other regression edges. |
2020
| Custom template overrides | 1 | `custom_security.yaml` | Template override coverage for `--template-dir`. |
2121
| Timestamp suppression | 1 | `simple.yaml` | Fixtures that exercise `--disable-timestamp`. |

fastapi_code_generator/parser.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,42 @@ def parse_request_body(
626626
self._temporary_operation['_request'] = arguments[0] if arguments else None
627627
return request_body_fields
628628

629+
def get_field_extras(self, obj: JsonSchemaObject) -> dict[str, Any]:
630+
extras = super().get_field_extras(obj)
631+
if self._has_non_object_discriminator_variant(obj):
632+
extras.pop('discriminator', None)
633+
return extras
634+
635+
def _has_non_object_discriminator_variant(self, obj: JsonSchemaObject) -> bool:
636+
if not obj.discriminator:
637+
return False
638+
combined_schemas = [*(obj.oneOf or []), *(obj.anyOf or [])]
639+
if not combined_schemas:
640+
return False
641+
return any(
642+
not self._is_object_discriminator_variant(schema)
643+
for schema in combined_schemas
644+
)
645+
646+
def _is_object_discriminator_variant(
647+
self, schema: JsonSchemaObject, seen_refs: set[str] | None = None
648+
) -> bool:
649+
if seen_refs is None:
650+
seen_refs = set()
651+
if schema.ref:
652+
if schema.ref in seen_refs:
653+
return False
654+
seen_refs.add(schema.ref)
655+
schema = JsonSchemaObject.model_validate(self.get_ref_model(schema.ref))
656+
if schema.is_object or schema.properties:
657+
return True
658+
if not schema.allOf:
659+
return False
660+
return any(
661+
self._is_object_discriminator_variant(member, seen_refs.copy())
662+
for member in schema.allOf
663+
)
664+
629665
def _get_upload_file_type(
630666
self, schema: Union[JsonSchemaObject, ReferenceObject]
631667
) -> tuple[str, str]:

fastapi_code_generator/prompt_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@
509509
'\n'
510510
'| Format | Status | Evidence | Notes |\n'
511511
'|--------|--------|----------|-------|\n'
512-
'| OpenAPI YAML | tested | `tests/data/openapi/**/*.yaml` (25 '
512+
'| OpenAPI YAML | tested | `tests/data/openapi/**/*.yaml` (26 '
513513
'fixtures) | Primary fixture format exercised under '
514514
'`tests/data/openapi/**/*.yaml`. |\n'
515515
'| OpenAPI JSON | tested | '
@@ -525,7 +525,7 @@
525525
'\n'
526526
'| Suite | Fixtures | Example files | Notes |\n'
527527
'|-------|----------|---------------|-------|\n'
528-
'| Default template | 17 | `body_and_parameters.yaml`, '
528+
'| Default template | 18 | `body_and_parameters.yaml`, '
529529
'`content_in_parameters.yaml`, '
530530
'`content_in_parameters_inline.yaml` | Core single-file '
531531
'generation scenarios exercised by the main CLI tests. |\n'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# generated by fastapi-codegen:
2+
# filename: discriminated_union_simple_type.yaml
3+
# timestamp: 2020-06-19T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from fastapi import FastAPI
8+
9+
app = FastAPI(
10+
title='discriminated_union_simple_type',
11+
version='1.0.0',
12+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# generated by fastapi-codegen:
2+
# filename: discriminated_union_simple_type.yaml
3+
# timestamp: 2020-06-19T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Union
8+
9+
from pydantic import BaseModel, Field, RootModel
10+
11+
12+
class ImageUrl(BaseModel):
13+
kind: str
14+
image_url: str
15+
16+
17+
class AudioUrl(BaseModel):
18+
kind: str
19+
audio_url: str
20+
21+
22+
class DocumentUrl(BaseModel):
23+
kind: str
24+
document_url: str
25+
26+
27+
class BinaryContent(BaseModel):
28+
kind: str
29+
binary_content: str
30+
31+
32+
class Content(RootModel[Union[str, ImageUrl, AudioUrl, DocumentUrl, BinaryContent]]):
33+
root: Union[str, ImageUrl, AudioUrl, DocumentUrl, BinaryContent] = Field(
34+
..., description='Union type representing different content formats'
35+
)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
info:
2+
title: discriminated_union_simple_type
3+
version: 1.0.0
4+
openapi: 3.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
Content:
9+
oneOf:
10+
- type: string
11+
- $ref: '#/components/schemas/ImageUrl'
12+
- $ref: '#/components/schemas/AudioUrl'
13+
- $ref: '#/components/schemas/DocumentUrl'
14+
- $ref: '#/components/schemas/BinaryContent'
15+
discriminator:
16+
propertyName: kind
17+
mapping:
18+
image_url: '#/components/schemas/ImageUrl'
19+
audio_url: '#/components/schemas/AudioUrl'
20+
document_url: '#/components/schemas/DocumentUrl'
21+
binary_content: '#/components/schemas/BinaryContent'
22+
description: Union type representing different content formats
23+
ImageUrl:
24+
type: object
25+
properties:
26+
kind:
27+
type: string
28+
image_url:
29+
type: string
30+
required:
31+
- kind
32+
- image_url
33+
AudioUrl:
34+
type: object
35+
properties:
36+
kind:
37+
type: string
38+
audio_url:
39+
type: string
40+
required:
41+
- kind
42+
- audio_url
43+
DocumentUrl:
44+
type: object
45+
properties:
46+
kind:
47+
type: string
48+
document_url:
49+
type: string
50+
required:
51+
- kind
52+
- document_url
53+
BinaryContent:
54+
type: object
55+
properties:
56+
kind:
57+
type: string
58+
binary_content:
59+
type: string
60+
required:
61+
- kind
62+
- binary_content

tests/main/test_main.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import importlib.util
34
import json
45
from contextlib import contextmanager
56
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -45,6 +46,17 @@ def assert_specific_tag_routers_generated(output_dir: Path) -> None:
4546
validate_generated_code(output_dir)
4647

4748

49+
def assert_generated_module_has_attribute(
50+
module_path: Path, module_name: str, attribute_name: str
51+
) -> None:
52+
spec = importlib.util.spec_from_file_location(module_name, module_path)
53+
assert spec is not None
54+
assert spec.loader is not None
55+
module = importlib.util.module_from_spec(spec)
56+
spec.loader.exec_module(module)
57+
assert hasattr(module, attribute_name)
58+
59+
4860
def assert_generated_draft_request_is_hashable(models_path: Path) -> None:
4961
namespace: dict[str, Any] = {}
5062
exec( # noqa: S102 - execute generated fixture code in a test namespace.
@@ -251,6 +263,24 @@ def test_generate_from_json_input(tmp_path: Path, output_dir: Path) -> None:
251263
validate_generated_code(output_dir)
252264

253265

266+
@freeze_time("2020-06-19")
267+
def test_generate_discriminated_union_with_simple_type(output_dir: Path) -> None:
268+
run_cli_and_assert(
269+
input_path=DATA_PATH
270+
/ OPEN_API_DEFAULT_TEMPLATE_DIR_NAME
271+
/ "discriminated_union_simple_type.yaml",
272+
output_path=output_dir,
273+
expected_path=EXPECTED_OPENAPI_PATH
274+
/ "default_template"
275+
/ "discriminated_union_simple_type",
276+
)
277+
assert_generated_module_has_attribute(
278+
output_dir / "models.py",
279+
"generated_discriminated_union_simple_type",
280+
"Content",
281+
)
282+
283+
254284
@freeze_time("2020-06-19")
255285
def test_generate_escapes_aliases_in_parameter_defaults(output_dir: Path) -> None:
256286
spec = """openapi: 3.0.0

0 commit comments

Comments
 (0)