Skip to content
Open
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, 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.
*
* <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/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<ModelMap> allModels) {
super.postProcessOperationsWithModels(objs, allModels);
Expand Down
Original file line number Diff line number Diff line change
@@ -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}})
Loading
Loading