Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -1395,7 +1395,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
List<CodegenParameter> params = operation.allParams;

for (CodegenParameter cp : params) {
PydanticType pydantic = new PydanticType(
PydanticType pydantic = getPydanticParameterType(
cp,
modelImports,
exampleImports,
postponedModelImports,
Expand Down Expand Up @@ -1491,6 +1492,23 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
return objs;
}

protected PydanticType getPydanticParameterType(CodegenParameter parameter,
Set<String> modelImports,
Set<String> exampleImports,
Set<String> postponedModelImports,
Set<String> postponedExampleImports,
PythonImports moduleImports,
String classname) {
return new PydanticType(
modelImports,
exampleImports,
postponedModelImports,
postponedExampleImports,
moduleImports,
classname
);
}


@Override
public void postProcessParameter(CodegenParameter parameter) {
Expand Down Expand Up @@ -1861,7 +1879,7 @@ public String asTypeValue(PythonImports imports) {
* entries will be automatically removed.
*
* */
class PythonImports {
protected class PythonImports {
private Map<String, Set<String>> imports;

public PythonImports() {
Expand Down Expand Up @@ -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<String> modelImports;
private Set<String> exampleImports;
private Set<String> postponedModelImports;
private Set<String> postponedExampleImports;
private PythonImports moduleImports;
private String classname;
protected Set<String> modelImports;
protected Set<String> exampleImports;
protected Set<String> postponedModelImports;
protected Set<String> postponedExampleImports;
protected PythonImports moduleImports;
protected String classname;

public PydanticType(
Set<String> modelImports,
Expand All @@ -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()) {
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -1995,15 +2013,15 @@ 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"));
pt.addTypeParam(collectionItemType(cp.getItems()));
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");
Expand Down Expand Up @@ -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)
Expand All @@ -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");
Expand Down Expand Up @@ -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);

Expand All @@ -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");
}
Expand All @@ -2154,20 +2172,20 @@ 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());
exampleImports.add(cp.getDataType());
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.");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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, and header 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.
*
* <p>Request bodies and models are <em>not</em> wire-string values — they carry real JSON types —
* so they keep the strict base behaviour.
*/
protected class PydanticCoercibleType extends PydanticType {
public PydanticCoercibleType(
Set<String> modelImports,
Set<String> exampleImports,
Set<String> postponedModelImports,
Set<String> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,38 @@ public String getTypeDeclaration(Schema p) {
return super.getTypeDeclaration(p);
}

@Override
protected PydanticType getPydanticParameterType(CodegenParameter parameter,
Set<String> modelImports,
Set<String> exampleImports,
Set<String> postponedModelImports,
Set<String> postponedExampleImports,
PythonImports moduleImports,
String classname) {
// Path/query/header 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) {
Comment thread
JerrySLau marked this conversation as resolved.
Outdated
Comment thread
JerrySLau marked this conversation as resolved.
Outdated
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<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,9 +46,9 @@
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")
has_default: Annotated[Optional[str], 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")
no_default: Annotated[Optional[str], Field(description="no default value")] = Query(None, description="no default value", alias="noDefault")
,
) -> None:
""""""
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:
""""""
...
Loading
Loading