Skip to content

Commit f0232e8

Browse files
authored
Fix openapi schema sharing between optional and non-optional types (#314)
* Fix openapi schema sharing between optional and non-optional types * Move convert_type_info_to_openapi_schema declaration to SchemaRegistry
1 parent 1bfc38f commit f0232e8

File tree

6 files changed

+290
-100
lines changed

6 files changed

+290
-100
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [25.0.1] - 2024-07-28
8+
- Fix openapi schema sharing between optional and non-optional types
9+
710
## [25.0.0] - 2024-07-26
811
- Schemas extracted to OpenAPI components, are always referenced
912
- Better nullable handling in OpenAPI schemas

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "winter"
3-
version = "25.0.0"
3+
version = "25.0.1"
44
homepage = "https://github.com/WinterFramework/winter"
55
description = "Web Framework with focus on python typing, dataclasses and modular design"
66
authors = ["Alexander Egorov <[email protected]>"]

tests/winter_openapi/test_api_request_and_response_spec.py

Lines changed: 220 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -574,34 +574,73 @@ def simple_method(self, data: type_hint): # pragma: no cover
574574
assert result['components'] == expected_components
575575

576576

577+
@dataclass
578+
class DataclassWithUndefined:
579+
"""DataclassWithUndefined description"""
580+
nested: Union[Dataclass, Undefined]
581+
nested_2: Union[Dataclass, Undefined, None]
582+
583+
577584
def test_reuse_schema():
578585
class _TestAPI: # pragma: no cover
579-
@winter.route_get('/method_1/')
580-
def method_1(self) -> Dataclass:
586+
@winter.route_get('/method_return_1/')
587+
def method_return_1(self) -> Dataclass:
581588
pass
582589

583-
@winter.route_get('/method_2/')
584-
def method_2(self) -> Dataclass:
590+
@winter.route_get('/method_return_2/')
591+
def method_return_2(self) -> Dataclass:
592+
pass
593+
594+
@winter.route_get('/method_return_optional_1/')
595+
def method_return_optional_1(self) -> Optional[Dataclass]:
596+
pass
597+
598+
@winter.route_get('/method_return_optional_2/')
599+
def method_return_optional_2(self) -> Optional[Dataclass]:
600+
pass
601+
602+
@winter.route_get('/method_return_with_undefined_1/')
603+
def method_return_with_undefined_1(self) -> DataclassWithUndefined:
604+
pass
605+
606+
@winter.route_get('/method_return_with_undefined_2/')
607+
def method_return_with_undefined_2(self) -> DataclassWithUndefined:
608+
pass
609+
610+
@winter.route_post('/method_request_body_1/')
611+
@winter.request_body('data')
612+
def method_request_body_1(self, data: Dataclass):
613+
pass
614+
615+
@winter.route_post('/method_request_body_2/')
616+
@winter.request_body('data')
617+
def method_request_body_2(self, data: Dataclass):
585618
pass
586619

587-
@winter.route_post('/method_3/')
620+
@winter.route_post('/method_request_body_undefined_1/')
588621
@winter.request_body('data')
589-
def method_3(self, data: Dataclass):
622+
def method_request_body_undefined_1(self, data: DataclassWithUndefined):
590623
pass
591624

592-
@winter.route_post('/method_4/')
625+
@winter.route_post('/method_request_body_undefined_2/')
593626
@winter.request_body('data')
594-
def method_4(self, data: Dataclass):
627+
def method_request_body_undefined_2(self, data: DataclassWithUndefined):
595628
pass
596629

597630
result = generate_openapi(
598631
title='title',
599632
version='1.0.0',
600633
routes=[
601-
get_route(_TestAPI.method_1),
602-
get_route(_TestAPI.method_2),
603-
get_route(_TestAPI.method_3),
604-
get_route(_TestAPI.method_4),
634+
get_route(_TestAPI.method_return_1),
635+
get_route(_TestAPI.method_return_2),
636+
get_route(_TestAPI.method_return_optional_1),
637+
get_route(_TestAPI.method_return_optional_2),
638+
get_route(_TestAPI.method_return_with_undefined_1),
639+
get_route(_TestAPI.method_return_with_undefined_2),
640+
get_route(_TestAPI.method_request_body_1),
641+
get_route(_TestAPI.method_request_body_2),
642+
get_route(_TestAPI.method_request_body_undefined_1),
643+
get_route(_TestAPI.method_request_body_undefined_2),
605644
],
606645
)
607646
assert result == {
@@ -655,15 +694,48 @@ def method_4(self, data: Dataclass):
655694
'title': 'DataclassInput',
656695
'type': 'object',
657696
},
697+
'DataclassWithUndefined': {
698+
'description': 'DataclassWithUndefined description',
699+
'properties': {
700+
'nested': {
701+
'$ref': '#/components/schemas/Dataclass',
702+
},
703+
'nested_2': {
704+
'nullable': True,
705+
'allOf': [
706+
{'$ref': '#/components/schemas/Dataclass'},
707+
],
708+
},
709+
},
710+
'required': ['nested', 'nested_2'],
711+
'title': 'DataclassWithUndefined',
712+
'type': 'object',
713+
},
714+
'DataclassWithUndefinedInput': {
715+
'description': 'DataclassWithUndefined description',
716+
'properties': {
717+
'nested': {
718+
'$ref': '#/components/schemas/DataclassInput',
719+
},
720+
'nested_2': {
721+
'nullable': True,
722+
'allOf': [
723+
{'$ref': '#/components/schemas/DataclassInput'},
724+
],
725+
},
726+
},
727+
'title': 'DataclassWithUndefinedInput',
728+
'type': 'object',
729+
},
658730
},
659731
},
660732
'info': {'title': 'title', 'version': '1.0.0'},
661733
'openapi': '3.0.3',
662734
'paths': {
663-
'/method_1/': {
735+
'/method_return_1/': {
664736
'get': {
665737
'deprecated': False,
666-
'operationId': '_TestAPI.method_1',
738+
'operationId': '_TestAPI.method_return_1',
667739
'parameters': [],
668740
'responses': {
669741
'200': {
@@ -675,13 +747,13 @@ def method_4(self, data: Dataclass):
675747
'description': '',
676748
},
677749
},
678-
'tags': ['method_1'],
750+
'tags': ['method_return_1'],
679751
},
680752
},
681-
'/method_2/': {
753+
'/method_return_2/': {
682754
'get': {
683755
'deprecated': False,
684-
'operationId': '_TestAPI.method_2',
756+
'operationId': '_TestAPI.method_return_2',
685757
'parameters': [],
686758
'responses': {
687759
'200': {
@@ -693,13 +765,99 @@ def method_4(self, data: Dataclass):
693765
'description': '',
694766
},
695767
},
696-
'tags': ['method_2'],
768+
'tags': ['method_return_2'],
769+
},
770+
},
771+
'/method_return_optional_1/': {
772+
'get': {
773+
'deprecated': False,
774+
'operationId': '_TestAPI.method_return_optional_1',
775+
'parameters': [],
776+
'responses': {
777+
'200': {
778+
'content': {
779+
'application/json': {
780+
'schema': {
781+
'nullable': True,
782+
'allOf': [
783+
{'$ref': '#/components/schemas/Dataclass'},
784+
],
785+
},
786+
},
787+
},
788+
'description': '',
789+
},
790+
},
791+
'tags': ['method_return_optional_1'],
792+
},
793+
},
794+
'/method_return_optional_2/': {
795+
'get': {
796+
'deprecated': False,
797+
'operationId': '_TestAPI.method_return_optional_2',
798+
'parameters': [],
799+
'responses': {
800+
'200': {
801+
'content': {
802+
'application/json': {
803+
'schema': {
804+
'nullable': True,
805+
'allOf': [
806+
{'$ref': '#/components/schemas/Dataclass'},
807+
],
808+
},
809+
},
810+
},
811+
'description': '',
812+
},
813+
},
814+
'tags': ['method_return_optional_2'],
815+
},
816+
},
817+
'/method_return_with_undefined_1/': {
818+
'get': {
819+
'deprecated': False,
820+
'operationId': '_TestAPI.method_return_with_undefined_1',
821+
'parameters': [],
822+
'responses': {
823+
'200': {
824+
'content': {
825+
'application/json': {
826+
'schema': {
827+
'$ref': '#/components/schemas/DataclassWithUndefined',
828+
},
829+
},
830+
},
831+
'description': '',
832+
},
833+
},
834+
'tags': ['method_return_with_undefined_1'],
697835
},
698836
},
699-
'/method_3/': {
837+
'/method_return_with_undefined_2/': {
838+
'get': {
839+
'deprecated': False,
840+
'operationId': '_TestAPI.method_return_with_undefined_2',
841+
'parameters': [],
842+
'responses': {
843+
'200': {
844+
'content': {
845+
'application/json': {
846+
'schema': {
847+
'$ref': '#/components/schemas/DataclassWithUndefined',
848+
},
849+
},
850+
},
851+
'description': '',
852+
},
853+
},
854+
'tags': ['method_return_with_undefined_2'],
855+
},
856+
},
857+
'/method_request_body_1/': {
700858
'post': {
701859
'deprecated': False,
702-
'operationId': '_TestAPI.method_3',
860+
'operationId': '_TestAPI.method_request_body_1',
703861
'parameters': [],
704862
'requestBody': {
705863
'content': {
@@ -712,13 +870,13 @@ def method_4(self, data: Dataclass):
712870
'responses': {
713871
'200': {'description': ''},
714872
},
715-
'tags': ['method_3'],
873+
'tags': ['method_request_body_1'],
716874
},
717875
},
718-
'/method_4/': {
876+
'/method_request_body_2/': {
719877
'post': {
720878
'deprecated': False,
721-
'operationId': '_TestAPI.method_4',
879+
'operationId': '_TestAPI.method_request_body_2',
722880
'parameters': [],
723881
'requestBody': {
724882
'content': {
@@ -731,7 +889,45 @@ def method_4(self, data: Dataclass):
731889
'responses': {
732890
'200': {'description': ''},
733891
},
734-
'tags': ['method_4'],
892+
'tags': ['method_request_body_2'],
893+
},
894+
},
895+
'/method_request_body_undefined_1/': {
896+
'post': {
897+
'deprecated': False,
898+
'operationId': '_TestAPI.method_request_body_undefined_1',
899+
'parameters': [],
900+
'requestBody': {
901+
'content': {
902+
'application/json': {
903+
'schema': {'$ref': '#/components/schemas/DataclassWithUndefinedInput'},
904+
},
905+
},
906+
'required': False,
907+
},
908+
'responses': {
909+
'200': {'description': ''},
910+
},
911+
'tags': ['method_request_body_undefined_1'],
912+
},
913+
},
914+
'/method_request_body_undefined_2/': {
915+
'post': {
916+
'deprecated': False,
917+
'operationId': '_TestAPI.method_request_body_undefined_2',
918+
'parameters': [],
919+
'requestBody': {
920+
'content': {
921+
'application/json': {
922+
'schema': {'$ref': '#/components/schemas/DataclassWithUndefinedInput'},
923+
},
924+
},
925+
'required': False
926+
},
927+
'responses': {
928+
'200': {'description': ''},
929+
},
930+
'tags': ['method_request_body_undefined_2'],
735931
},
736932
},
737933
},

0 commit comments

Comments
 (0)