Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,24 @@ public void updateImportsFromCodegenModel(String modelName, CodegenModel cm, Set
}
}

/**
* Whether the given request parameter should be typed with coercible types
* ({@code int}/{@code str}/{@code float}) instead of Pydantic strict types
* ({@code StrictInt}/{@code StrictStr}/{@code StrictFloat}, {@code strict=True}).
*
* <p>The default is {@code false}, preserving strict typing for all generators
* (notably the Python client, which builds JSON request bodies where strict
* validation is desirable). Server generators that parse path/query/header values
* from the wire — where everything arrives as a string and relies on Pydantic
* coercion — should override this for non-body parameters. See issue #21905.
*
* @param parameter the request parameter being typed
* @return {@code true} to relax strict typing for this parameter
*/
protected boolean shouldRelaxStrictParameterTyping(CodegenParameter parameter) {
return false;
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
hasModelsToImport = false;
Expand All @@ -1324,7 +1342,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
postponedModelImports,
postponedExampleImports,
moduleImports,
null
null,
shouldRelaxStrictParameterTyping(cp)
);
String typing = pydantic.generatePythonType(cp);
cp.vendorExtensions.put(X_PY_TYPING, typing);
Expand Down Expand Up @@ -1843,6 +1862,11 @@ class PydanticType {
private Set<String> postponedExampleImports;
private PythonImports moduleImports;
private String classname;
// When true, emit coercible types (int/str/float) instead of Pydantic strict
// types (StrictInt/StrictStr/StrictFloat) and omit the strict=True constraint.
// Used for non-body request parameters, whose values always arrive as strings
// on the wire and rely on Pydantic's automatic coercion. See issue #21905.
private boolean relaxStrict;

public PydanticType(
Set<String> modelImports,
Expand All @@ -1851,13 +1875,26 @@ public PydanticType(
Set<String> postponedExampleImports,
PythonImports moduleImports,
String classname
) {
this(modelImports, exampleImports, postponedModelImports, postponedExampleImports, moduleImports, classname, false);
}

public PydanticType(
Set<String> modelImports,
Set<String> exampleImports,
Set<String> postponedModelImports,
Set<String> postponedExampleImports,
PythonImports moduleImports,
String classname,
boolean relaxStrict
) {
this.modelImports = modelImports;
this.exampleImports = exampleImports;
this.postponedModelImports = postponedModelImports;
this.postponedExampleImports = postponedExampleImports;
this.moduleImports = moduleImports;
this.classname = classname;
this.relaxStrict = relaxStrict;
}

private PythonType arrayType(IJsonSchemaValidationProperties cp) {
Expand Down Expand Up @@ -1904,7 +1941,9 @@ private PythonType stringType(IJsonSchemaValidationProperties cp) {
PythonType pt = new PythonType("str");

// e.g. constr(regex=r'/[a-z]/i', strict=True)
pt.constrain("strict", true);
if (!relaxStrict) {
pt.constrain("strict", true);
}
if (cp.getMaxLength() != null) {
pt.constrain("max_length", cp.getMaxLength());
}
Expand All @@ -1922,6 +1961,8 @@ private PythonType stringType(IJsonSchemaValidationProperties cp) {
if ("password".equals(cp.getFormat())) { // TODO avoid using format, use `is` boolean flag instead
moduleImports.add(PYDANTIC, "SecretStr");
return new PythonType("SecretStr");
} else if (relaxStrict) {
return new PythonType("str");
} else {
moduleImports.add(PYDANTIC, "StrictStr");
return new PythonType("StrictStr");
Expand Down Expand Up @@ -1966,30 +2007,43 @@ private PythonType numberType(IJsonSchemaValidationProperties cp) {
}

if ("Union[StrictFloat, StrictInt]".equals(mapNumberTo)) {
floatt.constrain("strict", true);
intt.constrain("strict", true);
if (!relaxStrict) {
floatt.constrain("strict", true);
intt.constrain("strict", true);
}

moduleImports.add(TYPING, "Union");
PythonType pt = new PythonType("Union");
pt.addTypeParam(floatt);
pt.addTypeParam(intt);
return pt;
} else if ("StrictFloat".equals(mapNumberTo)) {
floatt.constrain("strict", true);
if (!relaxStrict) {
floatt.constrain("strict", true);
}
return floatt;
} else { // float
return floatt;
}
} else {
if ("Union[StrictFloat, StrictInt]".equals(mapNumberTo)) {
moduleImports.add(TYPING, "Union");
if (relaxStrict) {
PythonType pt = new PythonType("Union");
pt.addTypeParam(new PythonType("float"));
pt.addTypeParam(new PythonType("int"));
return pt;
}
moduleImports.add(PYDANTIC, "StrictFloat");
moduleImports.add(PYDANTIC, "StrictInt");
PythonType pt = new PythonType("Union");
pt.addTypeParam(new PythonType("StrictFloat"));
pt.addTypeParam(new PythonType("StrictInt"));
return pt;
} else if ("StrictFloat".equals(mapNumberTo)) {
if (relaxStrict) {
return new PythonType("float");
}
moduleImports.add(PYDANTIC, "StrictFloat");
return new PythonType("StrictFloat");
} else {
Expand All @@ -2002,10 +2056,15 @@ private PythonType intType(IJsonSchemaValidationProperties cp) {
if (cp.getHasValidation()) {
PythonType pt = new PythonType("int");
// e.g. conint(ge=10, le=100, strict=True)
pt.constrain("strict", true);
if (!relaxStrict) {
pt.constrain("strict", true);
}
applyConstraints(pt, cp);
return pt;
} else {
if (relaxStrict) {
return new PythonType("int");
}
moduleImports.add(PYDANTIC, "StrictInt");
return new PythonType("StrictInt");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,18 @@ public String getTypeDeclaration(Schema p) {
return super.getTypeDeclaration(p);
}

/**
* Path/query/header parameters arrive on the wire as strings and rely on Pydantic's automatic
* coercion (e.g. {@code "3" -> 3}). Pydantic strict typing disables that coercion, making FastAPI
* reject otherwise-valid requests with a 422 ({@code int_type: Input should be a valid integer}).
* So relax strict typing for those parameters. Body parameters keep strict typing, since JSON
* request bodies carry real types and strict validation is desirable there. See issue #21905.
*/
@Override
protected boolean shouldRelaxStrictParameterTyping(CodegenParameter parameter) {
return parameter.isQueryParam || parameter.isPathParam || parameter.isHeaderParam;
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
super.postProcessOperationsWithModels(objs, allModels);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -46,8 +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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
""""""
...
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -95,7 +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(None, 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(None, description="Status values that need to be considered for filter", alias="status"),
token_petstore_auth: TokenModel = Security(
get_token_petstore_auth, scopes=["read:pets"]
),
Expand All @@ -117,7 +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(None, description="Tags to filter by", alias="tags"),
tags: Annotated[List[str], Field(description="Tags to filter by")] = Query(None, description="Tags to filter by", alias="tags"),
token_petstore_auth: TokenModel = Security(
get_token_petstore_auth, scopes=["read:pets"]
),
Expand All @@ -140,7 +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
),
Expand All @@ -161,7 +161,7 @@ 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"),
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"),
status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")] = Form(None, description="Updated status of the pet"),
token_petstore_auth: TokenModel = Security(
Expand All @@ -184,8 +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"]
),
Expand All @@ -206,7 +206,7 @@ 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"),
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"),
file: Optional[UploadFile] = File(None, description="file to upload"),
token_petstore_auth: TokenModel = Security(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -34,31 +34,31 @@ 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"""
...


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."""
...


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"""
...


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:
Expand All @@ -68,16 +68,16 @@ 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:
""""""
...


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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -87,7 +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 &lt;&#x3D; 5 or &gt; 10. Other values will generate exceptions"""
if not BaseStoreApi.subclasses:
Expand All @@ -106,7 +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 &lt; 1000. Anything above 1000 or nonintegers will generate API errors"""
if not BaseStoreApi.subclasses:
Expand Down
Loading
Loading