diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java index f2ab264952cd..b08a6f518719 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java @@ -1395,7 +1395,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List params = operation.allParams; for (CodegenParameter cp : params) { - PydanticType pydantic = new PydanticType( + PydanticType pydantic = getPydanticParameterType( + cp, modelImports, exampleImports, postponedModelImports, @@ -1491,6 +1492,23 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List modelImports, + Set exampleImports, + Set postponedModelImports, + Set postponedExampleImports, + PythonImports moduleImports, + String classname) { + return new PydanticType( + modelImports, + exampleImports, + postponedModelImports, + postponedExampleImports, + moduleImports, + classname + ); + } + @Override public void postProcessParameter(CodegenParameter parameter) { @@ -1861,7 +1879,7 @@ public String asTypeValue(PythonImports imports) { * entries will be automatically removed. * * */ - class PythonImports { + protected class PythonImports { private Map> imports; public PythonImports() { @@ -1907,18 +1925,18 @@ public boolean isEmpty() { } } - class PydanticType { + protected class PydanticType { - private static final String TYPING = "typing"; + protected static final String TYPING = "typing"; - private static final String DECIMAL = "Decimal"; + protected static final String DECIMAL = "Decimal"; - private Set modelImports; - private Set exampleImports; - private Set postponedModelImports; - private Set postponedExampleImports; - private PythonImports moduleImports; - private String classname; + protected Set modelImports; + protected Set exampleImports; + protected Set postponedModelImports; + protected Set postponedExampleImports; + protected PythonImports moduleImports; + protected String classname; public PydanticType( Set modelImports, @@ -1936,7 +1954,7 @@ public PydanticType( this.classname = classname; } - private PythonType arrayType(IJsonSchemaValidationProperties cp) { + protected PythonType arrayType(IJsonSchemaValidationProperties cp) { PythonType pt = new PythonType(); ConstraintApplier.applyConstraints(cp, pt, ConstraintType.ARRAY); if (cp.getUniqueItems()) { @@ -1958,7 +1976,7 @@ private PythonType arrayType(IJsonSchemaValidationProperties cp) { return pt; } - private PythonType collectionItemType(CodegenProperty itemCp) { + protected PythonType collectionItemType(CodegenProperty itemCp) { PythonType itemPt = getType(itemCp); if (itemCp != null && !itemPt.type.equals("Any") && itemCp.isNullable) { moduleImports.add(TYPING, "Optional"); @@ -1969,7 +1987,7 @@ private PythonType collectionItemType(CodegenProperty itemCp) { return itemPt; } - private PythonType stringType(IJsonSchemaValidationProperties cp) { + protected PythonType stringType(IJsonSchemaValidationProperties cp) { if (cp.getHasValidation()) { PythonType pt = new PythonType("str"); @@ -1995,7 +2013,7 @@ private PythonType stringType(IJsonSchemaValidationProperties cp) { } } - private PythonType mapType(IJsonSchemaValidationProperties cp) { + protected PythonType mapType(IJsonSchemaValidationProperties cp) { moduleImports.add(TYPING, "Dict"); PythonType pt = new PythonType("Dict"); pt.addTypeParam(new PythonType("str")); @@ -2003,7 +2021,7 @@ private PythonType mapType(IJsonSchemaValidationProperties cp) { return pt; } - private PythonType numberType(IJsonSchemaValidationProperties cp) { + protected PythonType numberType(IJsonSchemaValidationProperties cp) { if (cp.getHasValidation()) { PythonType floatt = new PythonType("float"); PythonType intt = new PythonType("int"); @@ -2049,7 +2067,7 @@ private PythonType numberType(IJsonSchemaValidationProperties cp) { } } - private PythonType intType(IJsonSchemaValidationProperties cp) { + protected PythonType intType(IJsonSchemaValidationProperties cp) { if (cp.getHasValidation()) { PythonType pt = new PythonType("int"); // e.g. conint(ge=10, le=100, strict=True) @@ -2062,7 +2080,7 @@ private PythonType intType(IJsonSchemaValidationProperties cp) { } } - private PythonType binaryType(IJsonSchemaValidationProperties cp) { + protected PythonType binaryType(IJsonSchemaValidationProperties cp) { if (cp.getHasValidation()) { PythonType bytest = new PythonType("bytes"); PythonType strt = new PythonType("str"); @@ -2120,12 +2138,12 @@ private PythonType binaryType(IJsonSchemaValidationProperties cp) { } } - private PythonType boolType(IJsonSchemaValidationProperties cp) { + protected PythonType boolType(IJsonSchemaValidationProperties cp) { moduleImports.add(PYDANTIC, "StrictBool"); return new PythonType("StrictBool"); } - private PythonType decimalType(IJsonSchemaValidationProperties cp) { + protected PythonType decimalType(IJsonSchemaValidationProperties cp) { PythonType pt = new PythonType(DECIMAL); moduleImports.add("decimal", DECIMAL); @@ -2138,12 +2156,12 @@ private PythonType decimalType(IJsonSchemaValidationProperties cp) { return pt; } - private PythonType anyType(IJsonSchemaValidationProperties cp) { + protected PythonType anyType(IJsonSchemaValidationProperties cp) { moduleImports.add(TYPING, "Any"); return new PythonType("Any"); } - private PythonType dateType(IJsonSchemaValidationProperties cp) { + protected PythonType dateType(IJsonSchemaValidationProperties cp) { if (cp.getIsDate()) { moduleImports.add("datetime", "date"); } @@ -2154,12 +2172,12 @@ private PythonType dateType(IJsonSchemaValidationProperties cp) { return new PythonType(cp.getDataType()); } - private PythonType uuidType(IJsonSchemaValidationProperties cp) { + protected PythonType uuidType(IJsonSchemaValidationProperties cp) { moduleImports.add("uuid", "UUID"); return new PythonType("UUID"); } - private PythonType modelType(IJsonSchemaValidationProperties cp) { + protected PythonType modelType(IJsonSchemaValidationProperties cp) { // add model prefix hasModelsToImport = true; modelImports.add(cp.getDataType()); @@ -2167,7 +2185,7 @@ private PythonType modelType(IJsonSchemaValidationProperties cp) { return new PythonType(cp.getDataType()); } - private PythonType fromCommon(IJsonSchemaValidationProperties cp) { + protected PythonType fromCommon(IJsonSchemaValidationProperties cp) { if (cp == null) { // if codegen property (e.g. map/dict of undefined type) is null, default to string LOGGER.warn("Codegen property is null (e.g. map/dict of undefined type). Default to typing.Any."); @@ -2225,7 +2243,7 @@ public String generatePythonType(CodegenProperty cp) { return this.finalizeType(cp, pt); } - private PythonType getType(CodegenProperty cp) { + protected PythonType getType(CodegenProperty cp) { PythonType result = fromCommon(cp); /* comment out the following since Literal requires python 3.8 @@ -2339,7 +2357,7 @@ public String generatePythonType(CodegenParameter cp) { return this.finalizeType(cp, pt); } - private PythonType getType(CodegenParameter cp) { + protected PythonType getType(CodegenParameter cp) { // TODO: cleanup PythonType result = fromCommon(cp); @@ -2474,4 +2492,108 @@ private static int floorValue(String value) { return (int) Math.floor(Double.parseDouble(value)); } } + + /** + * Pydantic type generator for values that arrive over the wire as strings — server-bound request + * parameters in path, query, header, and cookie position. These rely on Pydantic's automatic coercion + * (e.g. {@code "3" -> 3}); the strict types emitted by the base {@link PydanticType} + * ({@code StrictInt}/{@code StrictStr}/{@code StrictFloat}, {@code strict=True}) disable that + * coercion and make FastAPI reject otherwise-valid requests with a 422. See issue #21905. + * + *

Request bodies and models are not wire-string values — they carry real JSON types — + * so they keep the strict base behaviour. + */ + protected class PydanticCoercibleType extends PydanticType { + public PydanticCoercibleType( + Set modelImports, + Set exampleImports, + Set postponedModelImports, + Set postponedExampleImports, + PythonImports moduleImports, + String classname + ) { + super(modelImports, exampleImports, postponedModelImports, postponedExampleImports, moduleImports, classname); + } + + @Override + protected PythonType stringType(IJsonSchemaValidationProperties cp) { + if (cp.getHasValidation()) { + PythonType pt = new PythonType("str"); + ConstraintApplier.applyConstraints(cp, pt, ConstraintType.STRING); + if (cp.getPattern() != null) { + moduleImports.add(PYDANTIC, "field_validator"); + } + return pt; + } else if ("password".equals(cp.getFormat())) { // TODO avoid using format, use `is` boolean flag instead + moduleImports.add(PYDANTIC, "SecretStr"); + return new PythonType("SecretStr"); + } + + return new PythonType("str"); + } + + @Override + protected PythonType numberType(IJsonSchemaValidationProperties cp) { + if (cp.getHasValidation()) { + PythonType floatt = new PythonType("float"); + PythonType intt = new PythonType("int"); + + ConstraintApplier.applyConstraints(cp, floatt, ConstraintType.NUMBER); + ConstraintApplier.applyConstraints(cp, intt, ConstraintType.ROUNDED_NUMBER); + + if ("Union[StrictFloat, StrictInt]".equals(mapNumberTo)) { + moduleImports.add(TYPING, "Union"); + PythonType pt = new PythonType("Union"); + pt.addTypeParam(floatt); + pt.addTypeParam(intt); + return pt; + } else if ("StrictFloat".equals(mapNumberTo)) { + return floatt; + } else if (DECIMAL.equals(mapNumberTo)) { + return decimalType(cp); + } + + return floatt; + } else if ("Union[StrictFloat, StrictInt]".equals(mapNumberTo)) { + moduleImports.add(TYPING, "Union"); + PythonType pt = new PythonType("Union"); + pt.addTypeParam(new PythonType("float")); + pt.addTypeParam(new PythonType("int")); + return pt; + } else if ("StrictFloat".equals(mapNumberTo)) { + return new PythonType("float"); + } else if (DECIMAL.equals(mapNumberTo)) { + moduleImports.add("decimal", DECIMAL); + return new PythonType(DECIMAL); + } + + return new PythonType("float"); + } + + @Override + protected PythonType intType(IJsonSchemaValidationProperties cp) { + PythonType pt = new PythonType("int"); + if (cp.getHasValidation()) { + ConstraintApplier.applyConstraints(cp, pt, ConstraintType.NUMBER); + } + return pt; + } + + @Override + protected PythonType boolType(IJsonSchemaValidationProperties cp) { + return new PythonType("bool"); + } + + @Override + protected PythonType decimalType(IJsonSchemaValidationProperties cp) { + PythonType pt = new PythonType(DECIMAL); + moduleImports.add("decimal", DECIMAL); + + if (cp.getHasValidation()) { + ConstraintApplier.applyConstraints(cp, pt, ConstraintType.NUMBER); + } + + return pt; + } + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java index 5969ca789ecb..144e22ac4b8e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java @@ -242,6 +242,38 @@ public String getTypeDeclaration(Schema p) { return super.getTypeDeclaration(p); } + @Override + protected PydanticType getPydanticParameterType(CodegenParameter parameter, + Set modelImports, + Set exampleImports, + Set postponedModelImports, + Set postponedExampleImports, + PythonImports moduleImports, + String classname) { + // Path/query/header/cookie values always arrive as strings on the wire and rely on Pydantic + // coercion, so they must not use strict types. Body params keep the strict default. + if (parameter.isQueryParam || parameter.isPathParam || parameter.isHeaderParam || parameter.isCookieParam) { + return new PydanticCoercibleType( + modelImports, + exampleImports, + postponedModelImports, + postponedExampleImports, + moduleImports, + classname + ); + } + + return super.getPydanticParameterType( + parameter, + modelImports, + exampleImports, + postponedModelImports, + postponedExampleImports, + moduleImports, + classname + ); + } + @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { super.postProcessOperationsWithModels(objs, allModels); diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/endpoint_argument_definition.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/endpoint_argument_definition.mustache index e398eca12c82..e031fb676969 100644 --- a/modules/openapi-generator/src/main/resources/python-fastapi/endpoint_argument_definition.mustache +++ b/modules/openapi-generator/src/main/resources/python-fastapi/endpoint_argument_definition.mustache @@ -1 +1 @@ -{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}}: {{>param_type}} = {{#isPathParam}}Path{{/isPathParam}}{{#isHeaderParam}}Header{{/isHeaderParam}}{{#isFormParam}}{{#isFile}}File{{/isFile}}{{^isFile}}Form{{/isFile}}{{/isFormParam}}{{#isQueryParam}}Query{{/isQueryParam}}{{#isCookieParam}}Cookie{{/isCookieParam}}{{#isBodyParam}}Body{{/isBodyParam}}({{&defaultValue}}{{^defaultValue}}{{#required}}...{{/required}}{{^required}}None{{/required}}{{/defaultValue}}, description="{{description}}"{{#isQueryParam}}, alias="{{baseName}}"{{/isQueryParam}}{{#isFormParam}}, alias="{{baseName}}"{{/isFormParam}}{{#isLong}}{{#minimum}}, ge={{.}}{{/minimum}}{{#maximum}}, le={{.}}{{/maximum}}{{/isLong}}{{#isInteger}}{{#minimum}}, ge={{.}}{{/minimum}}{{#maximum}}, le={{.}}{{/maximum}}{{/isInteger}}{{#vendorExtensions.x-regex}}, regex=r"{{.}}"{{/vendorExtensions.x-regex}}{{#minLength}}, min_length={{.}}{{/minLength}}{{#maxLength}}, max_length={{.}}{{/maxLength}}{{^isBodyParam}}{{#vendorExtensions.x-py-example}}, examples=[{{{.}}}]{{/vendorExtensions.x-py-example}}{{/isBodyParam}}{{#isBodyParam}}{{#vendorExtensions.x-py-fastapi-example}}, examples=[{{{.}}}]{{/vendorExtensions.x-py-fastapi-example}}{{/isBodyParam}}) +{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}}: {{>param_type}} = {{#isPathParam}}Path{{/isPathParam}}{{#isHeaderParam}}Header{{/isHeaderParam}}{{#isFormParam}}{{#isFile}}File{{/isFile}}{{^isFile}}Form{{/isFile}}{{/isFormParam}}{{#isQueryParam}}Query{{/isQueryParam}}{{#isCookieParam}}Cookie{{/isCookieParam}}{{#isBodyParam}}Body{{/isBodyParam}}({{&defaultValue}}{{^defaultValue}}{{#required}}...{{/required}}{{^required}}None{{/required}}{{/defaultValue}}, description="{{description}}"{{#isQueryParam}}, alias="{{baseName}}"{{/isQueryParam}}{{#isFormParam}}, alias="{{baseName}}"{{/isFormParam}}{{#isLong}}{{#minimum}}, ge={{.}}{{/minimum}}{{#maximum}}, le={{.}}{{/maximum}}{{/isLong}}{{#isInteger}}{{#minimum}}, ge={{.}}{{/minimum}}{{#maximum}}, le={{.}}{{/maximum}}{{/isInteger}}{{#vendorExtensions.x-regex}}, regex=r"{{.}}"{{/vendorExtensions.x-regex}}{{#minLength}}, min_length={{.}}{{/minLength}}{{#maxLength}}, max_length={{.}}{{/maxLength}}{{^isBodyParam}}{{#vendorExtensions.x-py-example}}, examples=[{{{.}}}]{{/vendorExtensions.x-py-example}}{{/isBodyParam}}{{#isBodyParam}}{{#vendorExtensions.x-py-fastapi-example}}, examples=[{{{.}}}]{{/vendorExtensions.x-py-fastapi-example}}{{/isBodyParam}}) \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastAPIServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastAPIServerCodegenTest.java index 26b9f73e20be..72a330b29b60 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastAPIServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastAPIServerCodegenTest.java @@ -193,4 +193,79 @@ public void testBinaryResponseUsesBytesNotFile() throws IOException { assertFileContains(baseApi, "-> bytes"); assertFileNotContains(baseApi, "-> file"); } + + /** + * Verifies that parameters arriving on the wire as strings (path, query, header, cookie) + * are typed with coercible Pydantic types ({@code int}/{@code bool}/{@code str}) instead of + * strict ones ({@code StrictInt}/{@code StrictBool}/{@code StrictStr}, {@code strict=True}), + * which would disable Pydantic's string coercion and make FastAPI reject valid requests + * with a 422 (#21905). + * + *

Schema constraints (e.g. {@code ge}/{@code le}) must be preserved, while JSON body + * model properties must keep strict typing since bodies carry real JSON types. + */ + @Test(description = "path/query/header/cookie params use coercible types, not strict types (#21905)") + public void testWireStringParamsUseCoercibleTypes() throws IOException { + final DefaultCodegen codegen = new PythonFastAPIServerCodegen(); + final String outputPath = generateFiles(codegen, "src/test/resources/bugs/issue_21905.yaml"); + final Path api = Paths.get(outputPath + "src/openapi_server/apis/item_api.py"); + final Path baseApi = Paths.get(outputPath + "src/openapi_server/apis/item_api_base.py"); + final Path model = Paths.get(outputPath + "src/openapi_server/models/item.py"); + + assertFileExists(api); + assertFileExists(baseApi); + + // path param: coercible int + assertFileContains(api, "itemId: int = Path(..., description=\"\")"); + // query param: coercible int, constraints kept but no strict=True + assertFileContains(api, "limit: Optional[Annotated[int, Field(le=100, ge=1)]] = Query(None, description=\"\", alias=\"limit\", ge=1, le=100)"); + // header param: coercible bool + assertFileContains(api, "x_verbose: Optional[bool] = Header(None, description=\"\")"); + // cookie params: values also arrive as strings on the wire, so they must be coercible too + assertFileContains(api, "session_id: Optional[int] = Cookie(None, description=\"\")"); + assertFileContains(api, "dark_mode: Optional[bool] = Cookie(None, description=\"\")"); + + // no strict types anywhere in the endpoint signatures + assertFileNotContains(api, "StrictInt"); + assertFileNotContains(api, "StrictBool"); + assertFileNotContains(api, "StrictStr"); + assertFileNotContains(api, "strict=True"); + assertFileNotContains(baseApi, "StrictInt"); + assertFileNotContains(baseApi, "StrictBool"); + assertFileNotContains(baseApi, "StrictStr"); + assertFileNotContains(baseApi, "strict=True"); + + // JSON body model properties keep strict typing (real JSON types, no wire-string coercion) + assertFileContains(model, "count: Optional[StrictInt] = None"); + } + + /** + * Verifies that endpoint argument commas stay at the end of the parameter line instead of + * being wrapped onto a line of their own. The {@code endpoint_argument_definition} partial + * is included inline (followed by {@code ,}) in api.mustache, so a trailing newline in the + * partial leaks into the output and produces the broken ")\n," style (#22494). + */ + @Test(description = "endpoint argument commas stay at end of line, no newline before comma (#22494)") + public void testEndpointArgumentCommaStaysOnSameLine() throws IOException { + final DefaultCodegen codegen = new PythonFastAPIServerCodegen(); + final String outputPath = generateFiles(codegen, "src/test/resources/bugs/issue_21905.yaml"); + final Path api = Paths.get(outputPath + "src/openapi_server/apis/item_api.py"); + + assertFileExists(api); + + // NOTE: assertFileContains linearizes away newlines, so raw content checks are required here + final String content = Files.readString(api); + + // commas terminate the parameter line + Assert.assertTrue(content.contains("itemId: int = Path(..., description=\"\"),\n"), + "parameter line should end with a comma: " + api); + Assert.assertTrue(content.contains("session_id: Optional[int] = Cookie(None, description=\"\"),\n"), + "parameter line should end with a comma: " + api); + Assert.assertTrue(content.contains("dark_mode: Optional[bool] = Cookie(None, description=\"\"),\n"), + "parameter line should end with a comma: " + api); + + // the comma must never be wrapped onto its own line + Assert.assertFalse(content.contains("\n,\n"), + "comma wrapped onto its own line in: " + api); + } } diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_21905.yaml b/modules/openapi-generator/src/test/resources/bugs/issue_21905.yaml new file mode 100644 index 000000000000..55567d98388a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_21905.yaml @@ -0,0 +1,69 @@ +# Regression spec for #21905, consumed by +# PythonFastAPIServerCodegenTest#testWireStringParamsUseCoercibleTypes. +# +# Focus: parameters that arrive on the wire as strings (path, query, header, cookie) +# must be generated with coercible Pydantic types (int/bool/str). Strict types +# (StrictInt/StrictBool/StrictStr, strict=True) would disable Pydantic's string +# coercion and make FastAPI reject valid requests with a 422. +openapi: 3.0.3 +info: + title: Wire-string parameter coercion (#21905) + version: 1.0.0 +paths: + /items/{itemId}: + get: + operationId: getItem + tags: + - item + parameters: + # one parameter per wire-string location; all must come out non-strict + - name: itemId + in: path + required: true + schema: + type: integer + - name: limit + in: query + schema: + type: integer + # constraints must be preserved while strict=True is dropped + minimum: 1 + maximum: 100 + - name: x_verbose + in: header + schema: + type: boolean + - name: session_id + in: cookie + schema: + type: integer + - name: dark_mode + in: cookie + schema: + type: boolean + responses: + '200': + description: OK + /items: + post: + operationId: createItem + tags: + - item + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + responses: + '200': + description: OK +components: + schemas: + # counter-assertion: JSON bodies carry real JSON types, so model properties + # must keep strict typing (count stays Optional[StrictInt]) + Item: + type: object + properties: + count: + type: integer diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py index 966ef8084147..56b3e6e64381 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py @@ -23,7 +23,7 @@ ) from openapi_server.models.extra_models import TokenModel # noqa: F401 -from pydantic import Field, StrictStr +from pydantic import Field from typing import Any, Optional from typing_extensions import Annotated @@ -46,10 +46,8 @@ response_model_by_alias=True, ) async def fake_query_param_default( - has_default: Annotated[Optional[StrictStr], Field(description="has default value")] = Query('Hello World', description="has default value", alias="hasDefault") -, - no_default: Annotated[Optional[StrictStr], Field(description="no default value")] = Query(None, description="no default value", alias="noDefault") -, + has_default: Annotated[Optional[str], Field(description="has default value")] = Query('Hello World', description="has default value", alias="hasDefault"), + no_default: Annotated[Optional[str], Field(description="no default value")] = Query(None, description="no default value", alias="noDefault"), ) -> None: """""" if not BaseFakeApi.subclasses: diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api_base.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api_base.py index 1c71537aa944..e8343c5d1330 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api_base.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api_base.py @@ -2,7 +2,7 @@ from typing import ClassVar, Dict, List, Tuple # noqa: F401 -from pydantic import Field, StrictStr +from pydantic import Field from typing import Any, Optional from typing_extensions import Annotated @@ -15,8 +15,8 @@ def __init_subclass__(cls, **kwargs): BaseFakeApi.subclasses = BaseFakeApi.subclasses + (cls,) async def fake_query_param_default( self, - has_default: Annotated[Optional[StrictStr], Field(description="has default value")], - no_default: Annotated[Optional[StrictStr], Field(description="no default value")], + has_default: Annotated[Optional[str], Field(description="has default value")], + no_default: Annotated[Optional[str], Field(description="no default value")], ) -> None: """""" ... diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py index 7a6d8bdc4ea7..a550aa7a21d1 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py @@ -23,7 +23,7 @@ ) from openapi_server.models.extra_models import TokenModel # noqa: F401 -from pydantic import Field, StrictBytes, StrictInt, StrictStr, field_validator +from pydantic import Field, StrictBytes, StrictStr, field_validator from typing import Any, List, Optional, Tuple, Union from typing_extensions import Annotated from openapi_server.models.api_response import ApiResponse @@ -51,8 +51,7 @@ response_model_by_alias=True, ) async def update_pet( - pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(..., description="Pet object that needs to be added to the store") -, + pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(..., description="Pet object that needs to be added to the store"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), @@ -74,8 +73,7 @@ async def update_pet( response_model_by_alias=True, ) async def add_pet( - pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(..., description="Pet object that needs to be added to the store") -, + pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(..., description="Pet object that needs to be added to the store"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), @@ -97,8 +95,7 @@ async def add_pet( response_model_by_alias=True, ) async def find_pets_by_status( - status: Annotated[List[StrictStr], Field(description="Status values that need to be considered for filter")] = Query(..., description="Status values that need to be considered for filter", alias="status") -, + status: Annotated[List[str], Field(description="Status values that need to be considered for filter")] = Query(..., description="Status values that need to be considered for filter", alias="status"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["read:pets"] ), @@ -120,8 +117,7 @@ async def find_pets_by_status( response_model_by_alias=True, ) async def find_pets_by_tags( - tags: Annotated[List[StrictStr], Field(description="Tags to filter by")] = Query(..., description="Tags to filter by", alias="tags") -, + tags: Annotated[List[str], Field(description="Tags to filter by")] = Query(..., description="Tags to filter by", alias="tags"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["read:pets"] ), @@ -144,8 +140,7 @@ async def find_pets_by_tags( response_model_by_alias=True, ) async def get_pet_by_id( - petId: Annotated[StrictInt, Field(description="ID of pet to return")] = Path(..., description="ID of pet to return") -, + petId: Annotated[int, Field(description="ID of pet to return")] = Path(..., description="ID of pet to return"), token_api_key: TokenModel = Security( get_token_api_key ), @@ -166,12 +161,9 @@ async def get_pet_by_id( response_model_by_alias=True, ) async def update_pet_with_form( - petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")] = Path(..., description="ID of pet that needs to be updated") -, - name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")] = Form(None, description="Updated name of the pet", alias="name") -, - status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")] = Form(None, description="Updated status of the pet", alias="status") -, + petId: Annotated[int, Field(description="ID of pet that needs to be updated")] = Path(..., description="ID of pet that needs to be updated"), + name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")] = Form(None, description="Updated name of the pet", alias="name"), + status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")] = Form(None, description="Updated status of the pet", alias="status"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), @@ -192,10 +184,8 @@ async def update_pet_with_form( response_model_by_alias=True, ) async def delete_pet( - petId: Annotated[StrictInt, Field(description="Pet id to delete")] = Path(..., description="Pet id to delete") -, - api_key: Optional[StrictStr] = Header(None, description="") -, + petId: Annotated[int, Field(description="Pet id to delete")] = Path(..., description="Pet id to delete"), + api_key: Optional[str] = Header(None, description=""), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), @@ -216,12 +206,9 @@ async def delete_pet( response_model_by_alias=True, ) async def upload_file( - petId: Annotated[StrictInt, Field(description="ID of pet to update")] = Path(..., description="ID of pet to update") -, - additional_metadata: Annotated[Optional[StrictStr], Field(description="Additional data to pass to server")] = Form(None, description="Additional data to pass to server", alias="additionalMetadata") -, - file: Optional[UploadFile] = File(None, description="file to upload", alias="file") -, + petId: Annotated[int, Field(description="ID of pet to update")] = Path(..., description="ID of pet to update"), + additional_metadata: Annotated[Optional[StrictStr], Field(description="Additional data to pass to server")] = Form(None, description="Additional data to pass to server", alias="additionalMetadata"), + file: Optional[UploadFile] = File(None, description="file to upload", alias="file"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py index 4f8b292e3eec..dd8780dc2416 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py @@ -2,7 +2,7 @@ from typing import ClassVar, Dict, List, Tuple # noqa: F401 -from pydantic import Field, StrictBytes, StrictInt, StrictStr, field_validator +from pydantic import Field, StrictBytes, StrictStr, field_validator from typing import Any, List, Optional, Tuple, Union from typing_extensions import Annotated from openapi_server.models.api_response import ApiResponse @@ -34,7 +34,7 @@ async def add_pet( async def find_pets_by_status( self, - status: Annotated[List[StrictStr], Field(description="Status values that need to be considered for filter")], + status: Annotated[List[str], Field(description="Status values that need to be considered for filter")], ) -> List[Pet]: """Multiple status values can be provided with comma separated strings""" ... @@ -42,7 +42,7 @@ async def find_pets_by_status( async def find_pets_by_tags( self, - tags: Annotated[List[StrictStr], Field(description="Tags to filter by")], + tags: Annotated[List[str], Field(description="Tags to filter by")], ) -> List[Pet]: """Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.""" ... @@ -50,7 +50,7 @@ async def find_pets_by_tags( async def get_pet_by_id( self, - petId: Annotated[StrictInt, Field(description="ID of pet to return")], + petId: Annotated[int, Field(description="ID of pet to return")], ) -> Pet: """Returns a single pet""" ... @@ -58,7 +58,7 @@ async def get_pet_by_id( async def update_pet_with_form( self, - petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")], + petId: Annotated[int, Field(description="ID of pet that needs to be updated")], name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")], status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")], ) -> None: @@ -68,8 +68,8 @@ async def update_pet_with_form( async def delete_pet( self, - petId: Annotated[StrictInt, Field(description="Pet id to delete")], - api_key: Optional[StrictStr], + petId: Annotated[int, Field(description="Pet id to delete")], + api_key: Optional[str], ) -> None: """""" ... @@ -77,7 +77,7 @@ async def delete_pet( async def upload_file( self, - petId: Annotated[StrictInt, Field(description="ID of pet to update")], + petId: Annotated[int, Field(description="ID of pet to update")], additional_metadata: Annotated[Optional[StrictStr], Field(description="Additional data to pass to server")], file: Optional[UploadFile], ) -> ApiResponse: diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py index 6dbefdcaa1a9..d1882a9005a8 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py @@ -23,7 +23,7 @@ ) from openapi_server.models.extra_models import TokenModel # noqa: F401 -from pydantic import Field, StrictInt, StrictStr +from pydantic import Field, StrictInt from typing import Any, Dict from typing_extensions import Annotated from openapi_server.models.order import Order @@ -67,8 +67,7 @@ async def get_inventory( response_model_by_alias=True, ) async def place_order( - order: Annotated[Order, Field(description="order placed for purchasing the pet")] = Body(..., description="order placed for purchasing the pet") -, + order: Annotated[Order, Field(description="order placed for purchasing the pet")] = Body(..., description="order placed for purchasing the pet"), ) -> Order: """""" if not BaseStoreApi.subclasses: @@ -88,8 +87,7 @@ async def place_order( response_model_by_alias=True, ) async def get_order_by_id( - orderId: Annotated[int, Field(le=5, strict=True, ge=1, description="ID of pet that needs to be fetched")] = Path(..., description="ID of pet that needs to be fetched", ge=1, le=5) -, + orderId: Annotated[int, Field(le=5, ge=1, description="ID of pet that needs to be fetched")] = Path(..., description="ID of pet that needs to be fetched", ge=1, le=5), ) -> Order: """For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions""" if not BaseStoreApi.subclasses: @@ -108,8 +106,7 @@ async def get_order_by_id( response_model_by_alias=True, ) async def delete_order( - orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")] = Path(..., description="ID of the order that needs to be deleted") -, + orderId: Annotated[str, Field(description="ID of the order that needs to be deleted")] = Path(..., description="ID of the order that needs to be deleted"), ) -> None: """For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors""" if not BaseStoreApi.subclasses: diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py index 84d9b639c1d3..76d310a4997c 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py @@ -2,7 +2,7 @@ from typing import ClassVar, Dict, List, Tuple # noqa: F401 -from pydantic import Field, StrictInt, StrictStr +from pydantic import Field, StrictInt from typing import Any, Dict from typing_extensions import Annotated from openapi_server.models.order import Order @@ -31,7 +31,7 @@ async def place_order( async def get_order_by_id( self, - orderId: Annotated[int, Field(le=5, strict=True, ge=1, description="ID of pet that needs to be fetched")], + orderId: Annotated[int, Field(le=5, ge=1, description="ID of pet that needs to be fetched")], ) -> Order: """For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions""" ... @@ -39,7 +39,7 @@ async def get_order_by_id( async def delete_order( self, - orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")], + orderId: Annotated[str, Field(description="ID of the order that needs to be deleted")], ) -> None: """For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors""" ... diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py index 06634461bfa4..c000022a85fe 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py @@ -46,8 +46,7 @@ response_model_by_alias=True, ) async def create_user( - user: Annotated[User, Field(description="Created user object")] = Body(..., description="Created user object") -, + user: Annotated[User, Field(description="Created user object")] = Body(..., description="Created user object"), token_api_key: TokenModel = Security( get_token_api_key ), @@ -68,8 +67,7 @@ async def create_user( response_model_by_alias=True, ) async def create_users_with_array_input( - user: Annotated[List[User], Field(description="List of user object")] = Body(..., description="List of user object") -, + user: Annotated[List[User], Field(description="List of user object")] = Body(..., description="List of user object"), token_api_key: TokenModel = Security( get_token_api_key ), @@ -90,8 +88,7 @@ async def create_users_with_array_input( response_model_by_alias=True, ) async def create_users_with_list_input( - user: Annotated[List[User], Field(description="List of user object")] = Body(..., description="List of user object") -, + user: Annotated[List[User], Field(description="List of user object")] = Body(..., description="List of user object"), token_api_key: TokenModel = Security( get_token_api_key ), @@ -113,10 +110,8 @@ async def create_users_with_list_input( response_model_by_alias=True, ) async def login_user( - username: Annotated[str, Field(strict=True, description="The user name for login")] = Query(..., description="The user name for login", alias="username", regex=r"^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$") -, - password: Annotated[StrictStr, Field(description="The password for login in clear text")] = Query(..., description="The password for login in clear text", alias="password") -, + username: Annotated[str, Field(description="The user name for login")] = Query(..., description="The user name for login", alias="username", regex=r"^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$"), + password: Annotated[str, Field(description="The password for login in clear text")] = Query(..., description="The password for login in clear text", alias="password"), ) -> str: """""" if not BaseUserApi.subclasses: @@ -156,8 +151,7 @@ async def logout_user( response_model_by_alias=True, ) async def get_user_by_name( - username: Annotated[StrictStr, Field(description="The name that needs to be fetched. Use user1 for testing.")] = Path(..., description="The name that needs to be fetched. Use user1 for testing.") -, + username: Annotated[str, Field(description="The name that needs to be fetched. Use user1 for testing.")] = Path(..., description="The name that needs to be fetched. Use user1 for testing."), ) -> User: """""" if not BaseUserApi.subclasses: @@ -176,10 +170,8 @@ async def get_user_by_name( response_model_by_alias=True, ) async def update_user( - username: Annotated[StrictStr, Field(description="name that need to be deleted")] = Path(..., description="name that need to be deleted") -, - user: Annotated[User, Field(description="Updated user object")] = Body(..., description="Updated user object") -, + username: Annotated[str, Field(description="name that need to be deleted")] = Path(..., description="name that need to be deleted"), + user: Annotated[User, Field(description="Updated user object")] = Body(..., description="Updated user object"), token_api_key: TokenModel = Security( get_token_api_key ), @@ -201,8 +193,7 @@ async def update_user( response_model_by_alias=True, ) async def delete_user( - username: Annotated[StrictStr, Field(description="The name that needs to be deleted")] = Path(..., description="The name that needs to be deleted") -, + username: Annotated[str, Field(description="The name that needs to be deleted")] = Path(..., description="The name that needs to be deleted"), token_api_key: TokenModel = Security( get_token_api_key ), diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py index 752960411104..9668b337d089 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py @@ -40,8 +40,8 @@ async def create_users_with_list_input( async def login_user( self, - username: Annotated[str, Field(strict=True, description="The user name for login")], - password: Annotated[StrictStr, Field(description="The password for login in clear text")], + username: Annotated[str, Field(description="The user name for login")], + password: Annotated[str, Field(description="The password for login in clear text")], ) -> str: """""" ... @@ -56,7 +56,7 @@ async def logout_user( async def get_user_by_name( self, - username: Annotated[StrictStr, Field(description="The name that needs to be fetched. Use user1 for testing.")], + username: Annotated[str, Field(description="The name that needs to be fetched. Use user1 for testing.")], ) -> User: """""" ... @@ -64,7 +64,7 @@ async def get_user_by_name( async def update_user( self, - username: Annotated[StrictStr, Field(description="name that need to be deleted")], + username: Annotated[str, Field(description="name that need to be deleted")], user: Annotated[User, Field(description="Updated user object")], ) -> None: """This can only be done by the logged in user.""" @@ -73,7 +73,7 @@ async def update_user( async def delete_user( self, - username: Annotated[StrictStr, Field(description="The name that needs to be deleted")], + username: Annotated[str, Field(description="The name that needs to be deleted")], ) -> None: """This can only be done by the logged in user.""" ...